aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2024-04-21 16:04:38 +0100
committerLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2024-04-21 16:04:38 +0100
commit1ab6ecba6f509b7b76865d65c77ecebc51efd2d3 (patch)
treea9b92e15769d483560d5799569b14c985b9c3ea5
downloadsentrum-1ab6ecba6f509b7b76865d65c77ecebc51efd2d3.tar.gz
sentrum-1ab6ecba6f509b7b76865d65c77ecebc51efd2d3.tar.bz2
sentrum-1ab6ecba6f509b7b76865d65c77ecebc51efd2d3.zip
Initial commitv0.1.0
-rw-r--r--.editorconfig9
-rw-r--r--.github/FUNDING.yml1
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md17
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md20
-rw-r--r--.github/PULL_REQUEST_TEMPLATE/pull_request_template.md29
-rw-r--r--.github/workflows/build.yml37
-rw-r--r--.github/workflows/ci.yml22
-rw-r--r--.github/workflows/deps.yml11
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock4242
-rw-r--r--Cargo.toml43
-rw-r--r--LICENSE.txt7
-rw-r--r--README.md361
-rw-r--r--contrib/sentrum.service16
-rw-r--r--docs/CHANGELOG.md3
-rw-r--r--docs/CODE_OF_CONDUCT.md3
-rw-r--r--docs/CONTRIBUTING.md27
-rw-r--r--docs/SECURITY.md13
-rw-r--r--docs/SUPPORT.md10
-rw-r--r--sentrum.sample.toml46
-rw-r--r--src/actions/command.rs61
-rw-r--r--src/actions/desktop_notification.rs28
-rw-r--r--src/actions/email.rs142
-rw-r--r--src/actions/mod.rs120
-rw-r--r--src/actions/nostr.rs192
-rw-r--r--src/actions/ntfy.rs119
-rw-r--r--src/actions/telegram.rs56
-rw-r--r--src/actions/terminal_print.rs28
-rw-r--r--src/blockchain.rs96
-rw-r--r--src/config.rs143
-rw-r--r--src/main.rs179
-rw-r--r--src/message.rs203
-rw-r--r--src/wallets.rs230
33 files changed, 6515 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..99580d0
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..033cde3
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+custom: pay.sommerfeld.dev
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..1848673
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,17 @@
+---
+name: Bug report
+about: Report a bug
+title: ''
+labels: bug
+assignees: sommerfelddev
+
+---
+
+**Bug Description**
+
+**To Reproduce**
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Environment**
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..85ccdfe
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea
+title: ''
+labels: enhancement
+assignees: sommerfelddev
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
new file mode 100644
index 0000000..dc6d5e4
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -0,0 +1,29 @@
+---
+name: Pull Request
+about: Submit a pull request
+title: ''
+---
+
+<!--
+Make sure you read CONTRIBUTING.md first.
+Never, merge master on your feature branch, always rebase and force-push.
+-->
+
+# Description
+
+<!--
+E.g:
+- Fixes ###
+- Enables/enhances ...
+- Tests ...
+- Improves ... related docs
+-->
+
+
+# Notes for reviewers
+
+<!--
+Add anything that might be relevant for reviewers to know about your code
+changes
+-->
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..02c9567
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,37 @@
+name: build every tag
+on:
+ push:
+ tags:
+ - '*'
+jobs:
+ build:
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+ - windows-latest
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@master
+ with:
+ toolchain: stable
+ - uses: Swatinem/rust-cache@v2
+ - run: cargo build --release
+ - if: matrix.os == 'windows-latest'
+ uses: actions/upload-artifact@v3
+ with:
+ path: target/release/sentrum.exe
+ name: sentrum.exe
+ - if: matrix.os == 'ubuntu-latest'
+ uses: actions/upload-artifact@v3
+ with:
+ path: target/release/sentrum*
+ name: sentrum_linux
+ - if: matrix.os == 'macos-latest'
+ uses: actions/upload-artifact@v3
+ with:
+ path: target/release/sentrum*
+ name: sentrum_macos
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..d51f1af
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,22 @@
+name: Rust
+
+on:
+ push:
+ branches: [ $default-branch ]
+ pull_request:
+ branches: [ $default-branch ]
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Build
+ run: cargo build --verbose
+ - name: Run tests
+ run: cargo test --verbose
diff --git a/.github/workflows/deps.yml b/.github/workflows/deps.yml
new file mode 100644
index 0000000..828fffa
--- /dev/null
+++ b/.github/workflows/deps.yml
@@ -0,0 +1,11 @@
+jobs:
+ latest_deps:
+ name: Latest Dependencies
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v3
+ - run: rustup update stable && rustup default stable
+ - run: cargo update --verbose
+ - run: cargo build --verbose
+ - run: cargo test --verbose
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..8cf2401
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,4242 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
+
+[[package]]
+name = "aquamarine"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a941c39708478e8eea39243b5983f1c42d2717b3620ee91f4a52115fd02ac43f"
+dependencies = [
+ "itertools",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb"
+dependencies = [
+ "event-listener 5.3.0",
+ "event-listener-strategy 0.5.1",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 5.3.0",
+ "event-listener-strategy 0.5.1",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1"
+dependencies = [
+ "async-lock",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884"
+dependencies = [
+ "async-lock",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b"
+dependencies = [
+ "event-listener 4.0.3",
+ "event-listener-strategy 0.4.0",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener 5.3.0",
+ "futures-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "async-scoped"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4042078ea593edffc452eef14e99fdb2b120caa4ad9618bcdeabc4a023b98740"
+dependencies = [
+ "futures",
+ "pin-project",
+ "tokio",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
+
+[[package]]
+name = "async-trait"
+version = "0.1.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "async-utility"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a349201d80b4aa18d17a34a182bdd7f8ddf845e9e57d2ea130a12e10ef1e3a47"
+dependencies = [
+ "futures-util",
+ "gloo-timers",
+ "tokio",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-wsocket"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c38341e6ee670913fb9dc3aba40c22d616261da4dc0928326d3168ebf576fb0"
+dependencies = [
+ "async-utility",
+ "futures-util",
+ "thiserror",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tokio-socks",
+ "tokio-tungstenite",
+ "url",
+ "wasm-ws",
+ "webpki-roots 0.26.1",
+]
+
+[[package]]
+name = "async_io_stream"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c"
+dependencies = [
+ "futures",
+ "pharos",
+ "rustc_version",
+]
+
+[[package]]
+name = "atomic-destructor"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4653a42bf04120a1d4e92452e006b4e3af4ab4afff8fb4af0f1bbb98418adf3e"
+dependencies = [
+ "tracing",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
+
+[[package]]
+name = "backtrace"
+version = "0.3.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
+
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
+name = "bdk"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fc1fc1a92e0943bfbcd6eb7d32c1b2a79f2f1357eb1e2eee9d7f36d6d7ca44a"
+dependencies = [
+ "async-trait",
+ "bdk-macros",
+ "bitcoin 0.30.2",
+ "electrum-client",
+ "getrandom",
+ "js-sys",
+ "log",
+ "miniscript",
+ "rand",
+ "serde",
+ "serde_json",
+ "sled",
+ "tokio",
+]
+
+[[package]]
+name = "bdk-macros"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81c1980e50ae23bb6efa9283ae8679d6ea2c6fa6a99fe62533f65f4a25a1a56c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "bech32"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
+
+[[package]]
+name = "bech32"
+version = "0.10.0-beta"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea"
+
+[[package]]
+name = "bip39"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f"
+dependencies = [
+ "bitcoin_hashes 0.11.0",
+ "serde",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "bitcoin"
+version = "0.30.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462"
+dependencies = [
+ "base64 0.13.1",
+ "bech32 0.9.1",
+ "bitcoin-private",
+ "bitcoin_hashes 0.12.0",
+ "hex_lit",
+ "secp256k1 0.27.0",
+ "serde",
+]
+
+[[package]]
+name = "bitcoin"
+version = "0.31.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae"
+dependencies = [
+ "bech32 0.10.0-beta",
+ "bitcoin-internals",
+ "bitcoin_hashes 0.13.0",
+ "hex-conservative",
+ "hex_lit",
+ "secp256k1 0.28.2",
+ "serde",
+]
+
+[[package]]
+name = "bitcoin-internals"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitcoin-private"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57"
+
+[[package]]
+name = "bitcoin_hashes"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
+
+[[package]]
+name = "bitcoin_hashes"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501"
+dependencies = [
+ "bitcoin-private",
+ "serde",
+]
+
+[[package]]
+name = "bitcoin_hashes"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
+dependencies = [
+ "bitcoin-internals",
+ "hex-conservative",
+ "serde",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
+dependencies = [
+ "async-channel",
+ "async-lock",
+ "async-task",
+ "fastrand",
+ "futures-io",
+ "futures-lite",
+ "piper",
+ "tracing",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
+[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
+dependencies = [
+ "aead",
+ "chacha20",
+ "cipher",
+ "poly1305",
+ "zeroize",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "chumsky"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
+dependencies = [
+ "hashbrown",
+ "stacker",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+ "zeroize",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim 0.11.1",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const_format"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673"
+dependencies = [
+ "const_format_proc_macros",
+]
+
+[[package]]
+name = "const_format_proc_macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim 0.10.0",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dptree"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d81175dab5ec79c30e0576df2ed2c244e1721720c302000bb321b107e82e265c"
+dependencies = [
+ "futures",
+]
+
+[[package]]
+name = "either"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
+
+[[package]]
+name = "electrum-client"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bc133f1c8d829d254f013f946653cbeb2b08674b960146361d1e9b67733ad19"
+dependencies = [
+ "bitcoin 0.30.2",
+ "bitcoin-private",
+ "byteorder",
+ "libc",
+ "log",
+ "rustls 0.21.11",
+ "serde",
+ "serde_json",
+ "webpki",
+ "webpki-roots 0.22.6",
+ "winapi",
+]
+
+[[package]]
+name = "email-encoding"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f"
+dependencies = [
+ "base64 0.22.0",
+ "memchr",
+]
+
+[[package]]
+name = "email_address"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
+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.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "erasable"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f11890ce181d47a64e5d1eb4b6caba0e7bae911a356723740d058a5d0340b7d"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
+dependencies = [
+ "event-listener 4.0.3",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3"
+dependencies = [
+ "event-listener 5.3.0",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-lite"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hex-conservative"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
+
+[[package]]
+name = "hex_lit"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "hostname"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
+dependencies = [
+ "libc",
+ "match_cfg",
+ "winapi",
+]
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http 1.1.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "human-panic"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4f016c89920bbb30951a8405ecacbb4540db5524313b9445736e7e1855cf370"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "backtrace",
+ "os_info",
+ "serde",
+ "serde_derive",
+ "toml",
+ "uuid",
+]
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "0.14.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
+dependencies = [
+ "futures-util",
+ "http 0.2.12",
+ "hyper 0.14.28",
+ "rustls 0.21.11",
+ "tokio",
+ "tokio-rustls 0.24.1",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
+dependencies = [
+ "futures-util",
+ "http 1.1.0",
+ "hyper 1.3.1",
+ "hyper-util",
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper 0.14.28",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "hyper 1.3.1",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core 0.52.0",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "itertools"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lettre"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47460276655930189e0919e4fbf46e46476b14f934f18a63dd726a5fb7b60e2e"
+dependencies = [
+ "async-trait",
+ "base64 0.22.0",
+ "chumsky",
+ "email-encoding",
+ "email_address",
+ "fastrand",
+ "futures-io",
+ "futures-util",
+ "hostname",
+ "httpdate",
+ "idna",
+ "mime",
+ "native-tls",
+ "nom",
+ "percent-encoding",
+ "quoted_printable",
+ "serde",
+ "socket2",
+ "tokio",
+ "tokio-native-tls",
+ "url",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.5.0",
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "lock_api"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "lru"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "mac-notification-sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64"
+dependencies = [
+ "cc",
+ "dirs-next",
+ "objc-foundation",
+ "objc_id",
+ "time",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "markdown"
+version = "1.0.0-alpha.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0f0025e8c0d89b84d6dc63e859475e40e8e82ab1a08be0a93ad5731513a508"
+dependencies = [
+ "unicode-id",
+]
+
+[[package]]
+name = "match_cfg"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
+
+[[package]]
+name = "memchr"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniscript"
+version = "10.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1eb102b66b2127a872dbcc73095b7b47aeb9d92f7b03c2b2298253ffc82c7594"
+dependencies = [
+ "bitcoin 0.30.2",
+ "bitcoin-private",
+ "serde",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "negentropy"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe"
+
+[[package]]
+name = "never"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
+
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags 2.5.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset",
+]
+
+[[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 = "nostr"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a27223888faca0c4ba9b97c2b7dc776e9a33d5f54e3558887471cf17798b5fbf"
+dependencies = [
+ "aes",
+ "base64 0.21.7",
+ "bip39",
+ "bitcoin 0.31.2",
+ "cbc",
+ "chacha20",
+ "chacha20poly1305",
+ "getrandom",
+ "instant",
+ "negentropy",
+ "once_cell",
+ "reqwest 0.12.4",
+ "scrypt",
+ "serde",
+ "serde_json",
+ "tracing",
+ "unicode-normalization",
+ "url",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "nostr-database"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f726b8c0904a838f64b51a931a1bf39e341f5584a5e04f06310fbfb847e2e924"
+dependencies = [
+ "async-trait",
+ "lru",
+ "nostr",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "nostr-relay-pool"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f0ccf9e81aa747abdfa130007651248b37c3699d37029bad701e68902257ce"
+dependencies = [
+ "async-utility",
+ "async-wsocket",
+ "atomic-destructor",
+ "nostr",
+ "nostr-database",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "nostr-sdk"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1ffedac7ab488e0dfea52804d0c43fafc7e3eefc62d97726d3927a1390db05b"
+dependencies = [
+ "async-utility",
+ "nostr",
+ "nostr-database",
+ "nostr-relay-pool",
+ "nostr-signer",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "nostr-signer"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e568670664cf5cc14a794ae32dfc04bde385d63ff0f5b1c3745dd3ea69f73a"
+dependencies = [
+ "async-utility",
+ "nostr",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "notify-rust"
+version = "4.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5312f837191c317644f313f7b2b39f9cb1496570c74f7c17152dd3961219551f"
+dependencies = [
+ "log",
+ "mac-notification-sys",
+ "serde",
+ "tauri-winrt-notification",
+ "zbus",
+]
+
+[[package]]
+name = "ntfy"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862d7410910ec279335692789a23b057a5046479242d3f9cb1dfd4c4b07f3a72"
+dependencies = [
+ "base64 0.21.7",
+ "chrono",
+ "reqwest 0.11.27",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "objc-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
+dependencies = [
+ "block",
+ "objc",
+ "objc_id",
+]
+
+[[package]]
+name = "objc_id"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
+dependencies = [
+ "objc",
+]
+
+[[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "openssl"
+version = "0.10.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+dependencies = [
+ "bitflags 2.5.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[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 = "os_info"
+version = "3.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092"
+dependencies = [
+ "log",
+ "serde",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core",
+ "subtle",
+]
+
+[[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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pharos"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414"
+dependencies = [
+ "futures",
+ "rustc_version",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "polling"
+version = "3.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "pretty_env_logger"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
+dependencies = [
+ "env_logger",
+ "log",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
+dependencies = [
+ "toml_edit 0.21.1",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "psm"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "quoted_printable"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rc-box"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0690759eabf094030c2cdabc25ade1395bac02210d920d655053c1d49583fd8"
+dependencies = [
+ "erasable",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+
+[[package]]
+name = "reqwest"
+version = "0.11.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
+dependencies = [
+ "base64 0.21.7",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.28",
+ "hyper-rustls 0.24.2",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "mime_guess",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls 0.21.11",
+ "rustls-pemfile 1.0.4",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-rustls 0.24.1",
+ "tokio-socks",
+ "tokio-util",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "webpki-roots 0.25.4",
+ "winreg 0.50.0",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
+dependencies = [
+ "base64 0.22.0",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "http-body-util",
+ "hyper 1.3.1",
+ "hyper-rustls 0.26.0",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls 0.22.4",
+ "rustls-pemfile 2.1.2",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tokio-socks",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots 0.26.1",
+ "winreg 0.52.0",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "spin",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
+dependencies = [
+ "bitflags 2.5.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.21.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-webpki 0.101.7",
+ "sct",
+]
+
+[[package]]
+name = "rustls"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki 0.102.2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64 0.21.7",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+dependencies = [
+ "base64 0.22.0",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+
+[[package]]
+name = "salsa20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "scopeguard"
+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 = [
+ "password-hash",
+ "pbkdf2",
+ "salsa20",
+ "sha2",
+]
+
+[[package]]
+name = "sct"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "secp256k1"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f"
+dependencies = [
+ "bitcoin_hashes 0.12.0",
+ "rand",
+ "secp256k1-sys 0.8.1",
+ "serde",
+]
+
+[[package]]
+name = "secp256k1"
+version = "0.28.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10"
+dependencies = [
+ "bitcoin_hashes 0.13.0",
+ "rand",
+ "secp256k1-sys 0.9.2",
+ "serde",
+]
+
+[[package]]
+name = "secp256k1-sys"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "secp256k1-sys"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
+
+[[package]]
+name = "send_wrapper"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+
+[[package]]
+name = "sentrum"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-scoped",
+ "async-trait",
+ "bdk",
+ "chrono",
+ "clap",
+ "const_format",
+ "dirs",
+ "human-panic",
+ "lettre",
+ "log",
+ "markdown",
+ "nostr-relay-pool",
+ "nostr-sdk",
+ "notify-rust",
+ "ntfy",
+ "pretty_env_logger",
+ "serde",
+ "strfmt",
+ "systemd-directories",
+ "teloxide",
+ "tokio",
+ "toml",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.198"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.198"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.116"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "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.60",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "sled"
+version = "0.34.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
+dependencies = [
+ "crc32fast",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "fs2",
+ "fxhash",
+ "libc",
+ "log",
+ "parking_lot",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "socket2"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "stacker"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "psm",
+ "winapi",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strfmt"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "systemd-directories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5469e2e28839738d6549f9eed6b48af8c11eb8d720b308ec01fd3c105c9f394"
+
+[[package]]
+name = "take_mut"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
+
+[[package]]
+name = "takecell"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e"
+
+[[package]]
+name = "tauri-winrt-notification"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f89f5fb70d6f62381f5d9b2ba9008196150b40b75f3068eb24faeddf1c686871"
+dependencies = [
+ "quick-xml",
+ "windows",
+ "windows-version",
+]
+
+[[package]]
+name = "teloxide"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c63345cf32a8850ebddcdd769dc2d5193d5e231262d5dada264b79da01a664da"
+dependencies = [
+ "aquamarine",
+ "bytes",
+ "derive_more",
+ "dptree",
+ "futures",
+ "log",
+ "mime",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "serde_with_macros",
+ "teloxide-core",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "url",
+]
+
+[[package]]
+name = "teloxide-core"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "303db260110c238e3af77bb9dff18bf7a5b5196f783059b0852aab75f91d5a16"
+dependencies = [
+ "bitflags 1.3.2",
+ "bytes",
+ "chrono",
+ "derive_more",
+ "either",
+ "futures",
+ "log",
+ "mime",
+ "never",
+ "once_cell",
+ "pin-project",
+ "rc-box",
+ "reqwest 0.11.27",
+ "serde",
+ "serde_json",
+ "serde_with_macros",
+ "take_mut",
+ "takecell",
+ "thiserror",
+ "tokio",
+ "tokio-util",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "time"
+version = "0.3.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
+dependencies = [
+ "rustls 0.21.11",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
+dependencies = [
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-socks"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
+dependencies = [
+ "either",
+ "futures-util",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tungstenite",
+ "webpki-roots 0.26.1",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit 0.22.12",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow 0.6.6",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http 1.1.0",
+ "httparse",
+ "log",
+ "rand",
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror",
+ "url",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+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",
+ "tempfile",
+ "winapi",
+]
+
+[[package]]
+name = "unicase"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-id"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "uuid"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-ws"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5b3a482e27ff54809c0848629d9033179705c5ea2f58e26cf45dc77c34c4984"
+dependencies = [
+ "async_io_stream",
+ "futures",
+ "js-sys",
+ "pharos",
+ "send_wrapper",
+ "thiserror",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.22.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
+dependencies = [
+ "webpki",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.25.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"
+dependencies = [
+ "windows-core 0.56.0",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-result",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "xdg-home"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "zbus"
+version = "4.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "derivative",
+ "enumflags2",
+ "event-listener 5.3.0",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "hex",
+ "nix",
+ "ordered-stream",
+ "rand",
+ "serde",
+ "serde_repr",
+ "sha1",
+ "static_assertions",
+ "tracing",
+ "uds_windows",
+ "windows-sys 0.52.0",
+ "xdg-home",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "4.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e0e3852c93dcdb49c9462afe67a2a468f7bd464150d866e861eaf06208633e0"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 1.0.109",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
+dependencies = [
+ "serde",
+ "static_assertions",
+ "zvariant",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+
+[[package]]
+name = "zvariant"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1b3ca6db667bfada0f1ebfc94b2b1759ba25472ee5373d4551bb892616389a"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "static_assertions",
+ "zvariant_derive",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7a4b236063316163b69039f77ce3117accb41a09567fd24c168e43491e521bc"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..ff4d23c
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,43 @@
+[package]
+name = "sentrum"
+version = "0.1.0"
+edition = "2021"
+authors = ["sommerfeld <sommerfeld@sommerfeld.dev>"]
+description = "Daemon that monitors watch-only bitcoin wallets"
+repository = "https://github.com/sommerfelddev/sentrum"
+license = "MIT"
+keywords = ["bitcoin", "notification", "daemon"]
+categories = ["command-line-utilities", "cryptography::cryptocurrencies"]
+
+[features]
+default = ["ntfy", "email", "telegram", "nostr", "desktop"]
+ntfy = ["dep:ntfy"]
+email = ["dep:lettre", "dep:markdown"]
+telegram = ["dep:teloxide"]
+nostr = ["dep:nostr-sdk", "dep:nostr-relay-pool"]
+desktop = ["dep:notify-rust"]
+
+[dependencies]
+anyhow = "1.0.81"
+bdk = "0.29.0"
+chrono = "0.4.37"
+clap = { version = "4.5.4", features = ["derive"] }
+const_format = "0.2.32"
+dirs = "5.0.1"
+human-panic = "1.2.3"
+lettre = { version = "0.11.6", features = ["serde", "tokio1", "tokio1-native-tls"], optional = true }
+log = "0.4.21"
+notify-rust = { version = "4.11.0", optional = true }
+ntfy = { version = "0.4.0", optional = true}
+pretty_env_logger = "0.5.0"
+serde = {version = "1.0.197", features = ["derive"] }
+strfmt = "0.2.4"
+systemd-directories = "0.1.1"
+toml = "0.8.12"
+markdown = { version = "1.0.0-alpha.16", optional = true }
+tokio = { version = "1.37.0", features = ["rt-multi-thread", "signal", "time"] }
+async-scoped = { version = "0.9.0", features = ["use-tokio"] }
+async-trait = "0.1.80"
+teloxide = { version = "0.12.2", optional = true }
+nostr-sdk = { version = "0.30.0", features = ["nip04", "nip05", "nip44", "nip59"], default-features = false, optional = true }
+nostr-relay-pool = { version = "0.30.0", optional = true }
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..47c682a
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,7 @@
+Copyright (c) 2024 sommerfeld
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b017306
--- /dev/null
+++ b/README.md
@@ -0,0 +1,361 @@
+# sentrum
+
+Daemon that monitors the Bitcoin blockchain for transactions involving your
+wallets and sends you notifications in many different channels (ntfy push
+notifications, email, telegram, nostr, arbitrary command, etc).
+
+## Installation
+
+Either:
+
+* Compile from source using `cargo install sentrum`
+* Download the binary from the [releases page](releases)
+* If using archlinux, install it from the [AUR](https://aur.archlinux.org/packages/sentrum)
+
+## Configuration
+
+### Config file path
+
+It will look for a `sentrum.toml` configuration file located in any of these
+directories (with this priority):
+
+1. Current working directory
+2. `$XDG_CONFIG_HOME/sentrum`
+3. `~/.config/sentrum`
+4. `/etc/sentrum` (more appropriate if running as a systemd service)
+
+Alternatively, you can pass the configuration file path as an argument in the
+invocation and that will override any of the above.
+
+**Start by copying the sample configuration to where you want it.** E.g.
+
+```bash
+cp sentrum.sample.toml sentrum.toml
+sudo cp sentrum.sample.toml /etc/sentrum/sentrum.toml
+```
+
+or
+
+```bash
+sudo cp sentrum.sample.toml /etc/sentrum/sentrum.toml
+```
+
+### What to configure
+
+You can use the [sentrum.sample.toml](sentrum.sample.toml) file as an
+example.
+Most options have very good defaults so you don't need to change them unless you
+want to. **In the examples below, commented options showcase their defaults,
+unless explicitly said otherwise.**
+
+#### Required
+
+* `wallets`: what wallets you want to monitor
+* `actions`: what actions you want to take once a relevant transaction is found
+
+#### Optional
+
+* `electrum`: by default, public electrum servers are used. You can configure it
+ to connect to your own
+* `message`: this allows you to configure the subject and body templates of the
+ notification message and choose the relevant data from the transaction that
+you want to include
+
+## Wallets
+
+For each wallet you want to track, add the following configuration:
+
+```toml
+[[wallets]]
+# Identifier for naming purposes (required)
+name = "alice"
+# Wallet xpub (required)
+xpub = "xpub6CkXHzuU1NyHUFNiQZLq2bgt6QPqjZbwpJ1MDgDeo4bWZ8ZP7HZr7v9WTLCQFhxVhqiJNcw5wSKE77rkAK1SzcuHjt36ZUibBHezGzGL9h9"
+# Script kind ("legacy","nested_segwit","segwit","taproot") (optional)
+#kind = "segwit"
+```
+
+It assumes a BIP84 (native segwit, `bc1` style addresses) wallet. If your wallet
+has a different script kind add the field `kind = "legacy"` (or `nested_segwit`,
+or `taproot`).
+
+More complex wallet types are supported by providing `primary = "<desc>"` and
+`change = "<desc>"` wallet descriptors instead of `xpub =` and `kind = `.
+
+## Actions
+
+For each new relevant transaction, you can take multiple actions. For each
+action you desire to take, you need to add the configuration:
+
+```toml
+[[actions]]
+# Action type (required)
+type = "<INSERT ACTION KIND>"
+<.... INSERT ACTION SPECIFIC CONFIGURATION HERE...>
+```
+
+Below we explain the configuration for each action kind. You can have multiple
+actions of the same kind (e.g. you want to send multiple emails from different
+accounts for some reason).
+
+### ntfy
+
+This is the best straightforward way to get push notifications on a smartphone.
+
+1. Install the android/iOS app following the relevant links in https://ntfy.sh
+2. If you don't run your own ntfy self-hosted server, create an account at
+ ntfy.sh
+3. Open the app, give it the needed permissions and configure your account
+ credentials
+4. Click on the `+` button and create a "topic", preferably named `sentrum`
+ since that's what will be used by default.
+
+Then you just need to add the relevant configuration:
+
+```toml
+[[actions]]
+type = "ntfy"
+# Credentials (required if you use a public server like the default one)
+credentials.username = "<YOUR USERNAME HERE>"
+credentials.password = "<YOUR PASSWORD HERE>"
+# ntfy server (optional)
+#url = "https://ntfy.sh"
+# notification channel name (optional)
+#topic = "sentrum"
+# Proxy used to connect (optional, defaults to None)
+#proxy = "socks5://127.0.0.1:9050"
+# Priority ("max", "high", "default", "low", "min") (optional)
+#priority = "default"
+```
+
+### nostr
+
+Get notified by a nostr [NIP04 encrypted
+DM](https://github.com/nostr-protocol/nips/blob/master/04.md) (leaks metadata
+but widely supported) or a
+[NIP59 GiftWrap sealed sender DM](https://github.com/nostr-protocol/nips/blob/master/59.md)
+(more private but not supported by many clients). Add:
+
+```toml
+[[actions]]
+type = "nostr"
+# Which npub to send the DM (required)
+recipient = "<YOUR npub, hex pubkey, nprofile or nip05>"
+# If NIP59 giftwrap DMs should be used instead of NIP04 (optional)
+#sealed_dm = false
+# Which relays to use to send DMs
+#relays = ["wss://nostr.bitcoiner.social", "wss://nostr.oxtr.dev", "wss://nostr.orangepill.dev", "wss://relay.damus.io"]
+```
+
+### email
+
+You need to add the configuration below and essentially configure an
+authenticated connection to your email provider's SMTP server. I cannot help you
+out with every provider's weird rules (maybe you need to allow 3rd party apps
+for gmail, who knows).
+
+```toml
+[[actions]]
+type = "email"
+# SMTP server (required)
+server = "<insert smtp server url (e.g. smtp.gmail.com)"
+# SMTP connection type ("tls", "starttls" or "plain") (optional)
+#connection = "tls"
+# SMTP port (optional, defaults to 587 for TLS, 465 for STARTTLS and 25 for plain connections
+#port = 1025
+# SMTP credentials (required in most cases)
+credentials.authentication_identity = "<insert login email>"
+credentials.secret = "<insert password>"
+# Accept self signed certificates (needed if you are using protonmail-bridge) (optional)
+#self_signed_cert = false
+# Configure sender (required)
+from = "sentrum <youremailhere@host.tld>"
+# Configure recipient (optional, defaults to the same as the "from" sender)
+#to = "sentrum <youremailhere@host.tld>"
+```
+
+### telegram
+
+1. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`.
+2. Optionally configure the bot (name, profile pic, etc) with @Botfather
+3. Open a chat with your bot
+4. Add the relevant config:
+
+```toml
+[[actions]]
+type = "telegram"
+# Auth token of the bot created with @Botfather (required)
+bot_token = "<insert bot token>"
+# 10-digit user id of the DM recipient, go to your profile to get it (required)
+user_id = 1234567890
+```
+
+### command
+
+Runs an external command where you can use transaction details as arguments.
+You can check what parameters (such as `{wallet}` or `{tx_net}` you can use in
+the [message](#message) configuration, since they are the same.
+
+```toml
+[[actions]]
+type = "command"
+cmd = "notify-send"
+args = ["[{wallet}] new tx: {tx_net} sats"]
+```
+
+
+### terminal_print
+
+Justs prints the notification text in the terminal. You can potentially pipe it
+to something else.
+
+```toml
+[[actions]]
+type = "terminal_print"
+```
+
+### desktop_notification
+
+Displays the transaction message as a native desktop notification on the same
+computer sentrum is running.
+
+```toml
+[[actions]]
+type = "desktop_notification"
+```
+
+## Message
+
+You can configure the message template and it applies to almost every action
+type. This configuration is entirely optional since the default templates will
+be used if omitted.
+
+Here is the default template:
+
+```toml
+[message]
+subject = "[{wallet}] new transaction"
+body = "net: {tx_net} sats, balance: {total_balance} sats, txid: {txid_short}"
+# Can be "plain", "markdown" or "html"
+format = "plain"
+# Configure blockexplorer urls. This is used to create the {tx_url} parameter
+block_explorers.mainnet = "https://mempool.space/tx/{txid}"
+block_explorers.testnet = "https://mempool.space/testnet/tx/{txid}"
+block_explorers.signet = "https://mempool.space/signet/tx/{txid}"
+```
+
+In the subject and body templates, you can use the following parameters:
+
+* `{tx_net}`: difference between the owned outputs and owned inputs
+* `{wallet}`: name of the configured wallet
+* `{total_balance}`: total balance of the wallet
+* `{txid}`: txid of the transaction
+* `{txid_short}`: truncated txid, easier on the eyes
+* `{received}`: sum of owned outputs
+* `{sent}`: sum of owned inputs
+* `{fee}`: transaction fee
+* `{current_height}`: current blockheight
+* `{tx_height}`: blockheight transaction confirmation
+* `{confs}`: number of transaction confirmations (0 for unconfirmed)
+* `{conf_timestamp}`: timestamp of the first confirmation in the `%Y-%m-%d %H:%M:%S` format
+* `{tx_url}`: a block explorer URL to the transaction
+
+## Electrum server
+
+By default, public electrum servers will be used. I **strongly suggest
+configuring your own electrum server if you want privacy (as you should)**.
+
+The defaults are:
+
+```toml
+[electrum]
+# Defaults:
+# - mainnet: ssl://fulcrum.sethforprivacy.com:50002
+# - testnet: ssl://electrum.blockstream.info:60002
+# - signet: ssl://mempool.space:60602
+# Use "tcp://" if you are connecting without SSL (e.g. "tcp://localhost:50001"
+# or "tcp://fwafiuesngirdghrdhgiurdhgirdgirdhgrd.onion:50001"
+url = "ssl://fulcrum.sethforprivacy.com:50002"
+# blockchain network ("bitcoin", "testnet", "signet", "regtest")
+network = "bitcoin"
+# Optional socks5 proxy (defaults to None)
+#socks5 = 127.0.0.1:9050
+# If using ssl with a trusted certificate, set this to true
+certificate_validation = false
+```
+
+# Usage
+
+Just run `sentrum` without arguments (uses default config search paths) or
+`sentrum <path/to/config/file>`.
+
+You can pass the `--test` flag to send a single test notification to all
+configured actions.
+
+By default, only new transactions can trigger actions. If you pass
+`--notify-past-txs`, it will send notifications of past transactions
+in the initial wallet sync. If you have a long transaction history, this will
+spam your notification channels for every transaction.
+
+## systemd service
+
+The ideal use-case is as a long running daemon, so it makes sense to configure
+it as a systemd service.
+
+If you are installing `sentrum` manually (e.g. from the releases page), you
+should:
+
+1. Create a new `sentrum` user:
+
+```bash
+sudo useradd -d /var/lib/sentrum -m sentrum
+```
+
+2. Place the `sentrum.toml` configuration file in `/etc/sentrum`:
+
+```bash
+sudo mkdir -p /etc/sentrum
+sudo cp sentrum.toml /etc/sentrum
+sudo chown -R sentrum:sentrum /etc/sentrum
+```
+
+3. Copy the [contrib/sentrum.service](contrib/sentrum.service) into the
+ `/etc/systemd/system`
+
+3. Reload systemd so that the service file can be found:
+
+```bash
+sudo systemclt daemon-reload
+```
+
+4. Enable and start the service:
+
+```bash
+sudo systemclt enable --now sentrum.service
+```
+
+5. Check if everything is fine with `systemctl status sentrum`
+
+6. Check the logs with `journalctl -fu sentrum`
+
+# Future Work
+
+* More action types:
+ - Matrix DM
+ - SimpleX chat DM
+ - IRC
+ - XMPP
+ - Whatsapp/Signal using linked devices (harder)
+ - HTTP request
+* More wallet types:
+ - Single Address (blocked by
+ https://github.com/bitcoindevkit/bdk/issues/759)
+ - Collections of wallets as a single entity
+* Notifications for the first tx confirmation and after N confirmations
+* Filtering notifications by the transaction amounts (e.g. no action for
+transactions smaller than 1M sats)
+* Debian package (using `cargo-deb`)
+* Allow per wallet actions
+* Support other blockchain backends (bitcoind-rpc, explora, block filters, dojo)
+* Maybe create a little web UI that helps with writing the configuration
+* Incentivize node distributions to package sentrum
diff --git a/contrib/sentrum.service b/contrib/sentrum.service
new file mode 100644
index 0000000..2d27524
--- /dev/null
+++ b/contrib/sentrum.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=sentrum daemon
+
+[Service]
+ExecStart=/usr/bin/sentrum
+User=sentrum
+
+# Hardening
+PrivateTmp=true
+ProtectSystem=full
+NoNewPrivileges=true
+PrivateDevices=true
+MemoryDenyWriteExecute=true
+
+[Install]
+WantedBy=multi-user.target
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
new file mode 100644
index 0000000..009131d
--- /dev/null
+++ b/docs/CHANGELOG.md
@@ -0,0 +1,3 @@
+0.1.0 (2024-04-21)
+------------------
+Initial release
diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..466b93b
--- /dev/null
+++ b/docs/CODE_OF_CONDUCT.md
@@ -0,0 +1,3 @@
+# Code of Conduct
+
+Behave and say however you want, if your code is good, you will not be canceled.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 0000000..e2d58ea
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,27 @@
+# Contributing
+
+## How to contribute
+
+1. Fork repo and create a new topic branch
+2. Make changes
+3. Ensure it compiles and passes tests using
+
+```bash
+cargo build
+cargo test
+```
+
+4. Auto format the code using `rustfmt` or a tool that integrates it (such as
+ `rust-analyzer` or some IDE).
+5. Make small atomic compilable working commits. Do NOT use "Conventional
+ Commits" for the commit title. Instead just directly write what was changed
+without any prefixes. Write it in the imperative tense and use the
+["50/72" rule](https://stackoverflow.com/a/11993051)
+6. Push commits to the created topic branch in your repo.
+7. Open a PR, wait for review.
+
+## How to create a new Action
+
+Use the [telegram action](../src/actions/telegram.rs) as a template. You need to
+implement the `Action` trait for your action and add the necessary hooks in
+[actions/mod.rs](../src/actions/mod.rs).
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 0000000..5586ce2
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,13 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| master | :white_check_mark: |
+| < master | :x: |
+
+## Reporting a Vulnerability
+
+If you discover a serious vulnerability,
+do not open an issue, instead contact the repository maintainer directly.
diff --git a/docs/SUPPORT.md b/docs/SUPPORT.md
new file mode 100644
index 0000000..a497321
--- /dev/null
+++ b/docs/SUPPORT.md
@@ -0,0 +1,10 @@
+# Bitcoin
+
+## On-chain:
+
+* paynym: [+mistymud0Bf](https://paynym.is/+mistymud0Bf)
+* [satsale sever](https://pay.sommerfeld.dev)
+
+## LN
+
+* Self-custodial lightning address: [sommerfeld@sommerfeld.dev](lightning:sommerfeld@sommerfeld.dev)
diff --git a/sentrum.sample.toml b/sentrum.sample.toml
new file mode 100644
index 0000000..63f4f17
--- /dev/null
+++ b/sentrum.sample.toml
@@ -0,0 +1,46 @@
+[[wallets]]
+# Identifier for naming purposes (required)
+name = "alice"
+# Wallet xpub (required)
+xpub = "xpub6CkXHzuU1NyHUFNiQZLq2bgt6QPqjZbwpJ1MDgDeo4bWZ8ZP7HZr7v9WTLCQFhxVhqiJNcw5wSKE77rkAK1SzcuHjt36ZUibBHezGzGL9h9"
+# Script kind ("legacy","nested_segwit","segwit","taproot") (optional)
+#kind = "segwit"
+
+# Another wallet
+#[[wallets]]
+#name = "bob"
+#xpub = "xpubblablabla"
+
+[[actions]]
+type = "terminal_print"
+
+# Add more actions here (ntfy, nostr, email, telegram, etc)
+#[[actions]]
+#type = "<INSERT ACTION KIND>"
+#<.... INSERT ACTION SPECIFIC CONFIGURATION HERE...>
+
+
+[message]
+subject = "[{wallet}] new transaction"
+body = "net: {tx_net} sats, balance: {total_balance} sats, txid: {txid_short}"
+# Can be "plain", "markdown" or "html"
+format = "plain"
+# Configure blockexplorer urls. This is used to create the {tx_url} parameter
+block_explorers.mainnet = "https://mempool.space/tx/{txid}"
+block_explorers.testnet = "https://mempool.space/testnet/tx/{txid}"
+block_explorers.signet = "https://mempool.space/signet/tx/{txid}"
+
+[electrum]
+# Defaults:
+# - mainnet: ssl://fulcrum.sethforprivacy.com:50002
+# - testnet: ssl://electrum.blockstream.info:60002
+# - signet: ssl://mempool.space:60602
+# Use "tcp://" if you are connecting without SSL (e.g. "tcp://localhost:50001"
+# or "tcp://fwafiuesngirdghrdhgiurdhgirdgirdhgrd.onion:50001"
+url = "ssl://fulcrum.sethforprivacy.com:50002"
+# blockchain network ("bitcoin", "testnet", "signet", "regtest")
+network = "bitcoin"
+# Optional socks5 proxy (defaults to None)
+#socks5 = 127.0.0.1:9050
+# If using ssl with a trusted certificate, set this to true
+certificate_validation = false
diff --git a/src/actions/command.rs b/src/actions/command.rs
new file mode 100644
index 0000000..28d951a
--- /dev/null
+++ b/src/actions/command.rs
@@ -0,0 +1,61 @@
+use std::collections::HashMap;
+use std::process::Command;
+
+use super::Action;
+use crate::message::MessageConfig;
+use crate::message::MessageParams;
+use anyhow::Result;
+use async_trait::async_trait;
+use serde::Deserialize;
+
+#[derive(Deserialize, Debug)]
+pub struct CommandConfig {
+ cmd: String,
+ #[serde(default)]
+ args: Vec<String>,
+ #[serde(default)]
+ clear_parent_env: bool,
+ #[serde(default)]
+ envs: HashMap<String, String>,
+ working_dir: Option<String>,
+}
+
+pub struct CommandAction<'a> {
+ message_config: &'a MessageConfig,
+ cmd_config: &'a CommandConfig,
+}
+
+impl<'a> CommandAction<'a> {
+ pub fn new(message_config: &'a MessageConfig, cmd_config: &'a CommandConfig) -> Result<Self> {
+ Ok(Self {
+ message_config,
+ cmd_config,
+ })
+ }
+}
+
+#[async_trait]
+impl Action<'_> for CommandAction<'_> {
+ async fn run(&self, params: Option<&MessageParams<'_, '_>>) -> Result<()> {
+ let mut cmd = Command::new(&self.cmd_config.cmd);
+ for arg in self.cmd_config.args.iter() {
+ cmd.arg(if let Some(p) = params {
+ self.message_config.replace_template_params(arg, p)?
+ } else {
+ arg.clone()
+ });
+ }
+
+ if self.cmd_config.clear_parent_env {
+ cmd.env_clear();
+ }
+ cmd.envs(&self.cmd_config.envs);
+
+ if let Some(working_dir) = &self.cmd_config.working_dir {
+ cmd.current_dir(working_dir);
+ }
+
+ cmd.status()?;
+ Ok(())
+ }
+}
diff --git a/src/actions/desktop_notification.rs b/src/actions/desktop_notification.rs
new file mode 100644
index 0000000..d831a59
--- /dev/null
+++ b/src/actions/desktop_notification.rs
@@ -0,0 +1,28 @@
+use super::Action;
+use crate::message::MessageConfig;
+use crate::message::MessageParams;
+use anyhow::Result;
+use async_trait::async_trait;
+
+#[derive(Debug)]
+pub struct DesktopNotificationAction<'a> {
+ message_config: &'a MessageConfig,
+}
+
+impl<'a> DesktopNotificationAction<'a> {
+ pub fn new(message_config: &'a MessageConfig) -> Self {
+ Self { message_config }
+ }
+}
+
+#[async_trait]
+impl Action<'_> for DesktopNotificationAction<'_> {
+ async fn run(&self, params: Option<&MessageParams<'_, '_>>) -> Result<()> {
+ use notify_rust::Notification;
+ Notification::new()
+ .summary(&self.message_config.subject(params)?)
+ .body(&self.message_config.body(params)?)
+ .show()?;
+ Ok(())
+ }
+}
diff --git a/src/actions/email.rs b/src/actions/email.rs
new file mode 100644
index 0000000..272f650
--- /dev/null
+++ b/src/actions/email.rs
@@ -0,0 +1,142 @@
+use super::Action;
+use crate::message::MessageConfig;
+use crate::message::MessageFormat;
+use crate::message::MessageParams;
+use anyhow::{Context, Result};
+use async_trait::async_trait;
+use lettre::message::header::ContentType;
+use lettre::message::MessageBuilder;
+use lettre::message::MultiPart;
+use lettre::message::SinglePart;
+use lettre::transport::smtp::authentication::Credentials;
+use lettre::transport::smtp::client::Tls;
+use lettre::transport::smtp::client::TlsParametersBuilder;
+use lettre::AsyncSmtpTransport;
+use lettre::AsyncTransport;
+use lettre::Message;
+use lettre::Tokio1Executor;
+use serde::Deserialize;
+
+#[derive(Deserialize, Debug, Copy, Clone)]
+#[serde(rename_all = "lowercase")]
+pub enum EmailConnectionType {
+ Plain,
+ StartTls,
+ Tls,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct EmailConfig {
+ server: String,
+ port: Option<u16>,
+ credentials: Option<Credentials>,
+ connection: Option<EmailConnectionType>,
+ self_signed_cert: Option<bool>,
+ from: String,
+ to: Option<String>,
+}
+
+impl EmailConfig {
+ pub fn server(&self) -> &str {
+ &self.server
+ }
+
+ pub fn connection(&self) -> EmailConnectionType {
+ self.connection.unwrap_or(EmailConnectionType::Tls)
+ }
+
+ pub fn self_signed_cert(&self) -> bool {
+ self.self_signed_cert.unwrap_or(false)
+ }
+
+ pub fn port(&self) -> u16 {
+ self.port.unwrap_or(match self.connection() {
+ EmailConnectionType::Tls => 587,
+ EmailConnectionType::StartTls => 465,
+ EmailConnectionType::Plain => 25,
+ })
+ }
+
+ pub fn to(&self) -> &str {
+ self.to.as_deref().unwrap_or(self.from.as_ref())
+ }
+}
+
+pub struct EmailAction<'a> {
+ message_config: &'a MessageConfig,
+ mailer: AsyncSmtpTransport<Tokio1Executor>,
+ message_builder: MessageBuilder,
+}
+impl<'a> EmailAction<'a> {
+ pub fn new(message_config: &'a MessageConfig, email_config: &'a EmailConfig) -> Result<Self> {
+ let tls_builder = TlsParametersBuilder::new(email_config.server().into())
+ .dangerous_accept_invalid_certs(email_config.self_signed_cert());
+ let tls_parameters = tls_builder.build()?;
+
+ let mut smtp_builder =
+ AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(email_config.server())
+ .port(email_config.port())
+ .tls(match email_config.connection() {
+ EmailConnectionType::Tls => Tls::Wrapper(tls_parameters),
+ EmailConnectionType::StartTls => Tls::Required(tls_parameters),
+ EmailConnectionType::Plain => Tls::None,
+ });
+ if let Some(cred) = &email_config.credentials {
+ smtp_builder = smtp_builder.credentials(cred.clone())
+ }
+ Ok(Self {
+ message_config,
+ mailer: smtp_builder.build(),
+ message_builder: Message::builder()
+ .from(
+ email_config
+ .from
+ .parse()
+ .with_context(|| format!("invalid from address '{}'", email_config.from))?,
+ )
+ .to(email_config
+ .to()
+ .parse()
+ .with_context(|| format!("invalid to address '{}'", email_config.to()))?),
+ })
+ }
+}
+
+#[async_trait]
+impl Action<'_> for EmailAction<'_> {
+ async fn run(&self, params: Option<&MessageParams<'_, '_>>) -> Result<()> {
+ let body = self.message_config.body(params)?;
+ let html_body = match self.message_config.format() {
+ MessageFormat::Markdown => format!(
+ "<!DOCTYPE html><html><body>{}</body></html>",
+ markdown::to_html(&body)
+ ),
+ MessageFormat::Html => body.clone(),
+ MessageFormat::Plain => Default::default(),
+ };
+ let email_builder = self
+ .message_builder
+ .clone()
+ .subject(self.message_config.subject(params)?);
+ let email = match self.message_config.format() {
+ MessageFormat::Plain => email_builder
+ .header(ContentType::TEXT_PLAIN)
+ .body(body.clone())?,
+ MessageFormat::Markdown | MessageFormat::Html => email_builder.multipart(
+ MultiPart::alternative()
+ .singlepart(
+ SinglePart::builder()
+ .header(ContentType::TEXT_PLAIN)
+ .body(body.clone()),
+ )
+ .singlepart(
+ SinglePart::builder()
+ .header(ContentType::TEXT_HTML)
+ .body(html_body.clone()),
+ ),
+ )?,
+ };
+ self.mailer.send(email).await?;
+ Ok(())
+ }
+}
diff --git a/src/actions/mod.rs b/src/actions/mod.rs
new file mode 100644
index 0000000..14ab279
--- /dev/null
+++ b/src/actions/mod.rs
@@ -0,0 +1,120 @@
+use std::fmt;
+
+use anyhow::Result;
+use async_trait::async_trait;
+use serde::Deserialize;
+
+use crate::message::MessageConfig;
+use crate::message::MessageParams;
+
+mod command;
+#[cfg(feature = "desktop")]
+mod desktop_notification;
+#[cfg(feature = "email")]
+mod email;
+#[cfg(feature = "nostr")]
+mod nostr;
+#[cfg(feature = "ntfy")]
+mod ntfy;
+#[cfg(feature = "telegram")]
+mod telegram;
+mod terminal_print;
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "snake_case")]
+#[serde(tag = "type")]
+pub enum AnyActionConfig {
+ TerminalPrint,
+ Command(self::command::CommandConfig),
+ #[cfg(feature = "desktop")]
+ DesktopNotification,
+ #[cfg(feature = "ntfy")]
+ Ntfy(self::ntfy::NtfyConfig),
+ #[cfg(feature = "email")]
+ Email(self::email::EmailConfig),
+ #[cfg(feature = "telegram")]
+ Telegram(self::telegram::TelegramConfig),
+ #[cfg(feature = "nostr")]
+ Nostr(self::nostr::NostrConfig),
+}
+
+impl fmt::Display for AnyActionConfig {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ AnyActionConfig::TerminalPrint => write!(f, "terminal_print"),
+ AnyActionConfig::Command(_) => write!(f, "command"),
+ #[cfg(feature = "desktop")]
+ AnyActionConfig::DesktopNotification => write!(f, "desktop_notification"),
+ #[cfg(feature = "ntfy")]
+ AnyActionConfig::Ntfy(_) => write!(f, "ntfy"),
+ #[cfg(feature = "email")]
+ AnyActionConfig::Email(_) => write!(f, "email"),
+ #[cfg(feature = "telegram")]
+ AnyActionConfig::Telegram(_) => write!(f, "telegram"),
+ #[cfg(feature = "nostr")]
+ AnyActionConfig::Nostr(_) => write!(f, "nostr"),
+ }
+ }
+}
+
+#[async_trait]
+pub trait Action<'a> {
+ async fn run(&self, params: Option<&MessageParams<'_, '_>>) -> Result<()>;
+}
+
+pub async fn get_action<'a>(
+ message_config: &'a MessageConfig,
+ action_config: &'a AnyActionConfig,
+) -> Result<Box<dyn Action<'a> + 'a + Sync>> {
+ Ok(match action_config {
+ AnyActionConfig::TerminalPrint => Box::new(self::terminal_print::TerminalPrintAction::new(
+ message_config,
+ )),
+ AnyActionConfig::Command(config) => {
+ Box::new(self::command::CommandAction::new(message_config, config)?)
+ }
+ #[cfg(feature = "desktop")]
+ AnyActionConfig::DesktopNotification => Box::new(
+ self::desktop_notification::DesktopNotificationAction::new(message_config),
+ ),
+ #[cfg(feature = "ntfy")]
+ AnyActionConfig::Ntfy(config) => {
+ Box::new(self::ntfy::NtfyAction::new(message_config, config)?)
+ }
+ #[cfg(feature = "email")]
+ AnyActionConfig::Email(config) => {
+ Box::new(self::email::EmailAction::new(message_config, config)?)
+ }
+ #[cfg(feature = "telegram")]
+ AnyActionConfig::Telegram(config) => {
+ Box::new(self::telegram::TelegramAction::new(message_config, config)?)
+ }
+ #[cfg(feature = "nostr")]
+ AnyActionConfig::Nostr(config) => {
+ Box::new(self::nostr::NostrAction::new(message_config, config).await?)
+ }
+ })
+}
+
+pub async fn get_actions<'a>(
+ message_config: &'a MessageConfig,
+ actions_config: &'a [AnyActionConfig],
+) -> Vec<Box<dyn Action<'a> + 'a + Sync>> {
+ let mut result: Vec<Box<dyn Action + Sync>> = Default::default();
+
+ // TODO: parallelize this. It's hard because the result vector needs to be shared.
+ for action_config in actions_config {
+ debug!("registering action '{}'", action_config);
+ match get_action(message_config, action_config).await {
+ Ok(action) => {
+ info!("registered action '{}'", action_config);
+ result.push(action);
+ }
+ Err(e) => {
+ warn!("could not register action '{}': {}", action_config, e);
+ }
+ }
+ }
+
+ result
+}
diff --git a/src/actions/nostr.rs b/src/actions/nostr.rs
new file mode 100644
index 0000000..aebccba
--- /dev/null
+++ b/src/actions/nostr.rs
@@ -0,0 +1,192 @@
+use super::Action;
+use crate::message::MessageConfig;
+use crate::message::MessageParams;
+use anyhow::{Context, Result};
+use async_scoped::TokioScope;
+use async_trait::async_trait;
+use const_format::formatcp;
+use nostr_relay_pool::RelayOptions;
+use nostr_sdk::nips::nip05;
+use nostr_sdk::serde_json::from_reader;
+use nostr_sdk::serde_json::to_string;
+use nostr_sdk::Client;
+use nostr_sdk::Keys;
+use nostr_sdk::Metadata;
+use nostr_sdk::PublicKey;
+use nostr_sdk::ToBech32;
+use serde::Deserialize;
+use serde::Serialize;
+use std::fs::File;
+use std::io::BufReader;
+use std::io::Write;
+use std::net::SocketAddr;
+use std::path::PathBuf;
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+struct NostrData {
+ key: String,
+ metadata_set: bool,
+}
+
+impl Default for NostrData {
+ fn default() -> Self {
+ NostrData {
+ key: Keys::generate().secret_key().unwrap().to_bech32().unwrap(),
+ metadata_set: false,
+ }
+ }
+}
+
+fn get_nostr_data_filepath() -> PathBuf {
+ dirs::cache_dir()
+ .unwrap_or(PathBuf::from("cache"))
+ .join(env!("CARGO_PKG_NAME"))
+ .join("nostr.json")
+}
+
+fn get_nostr_data() -> Result<NostrData> {
+ let path = get_nostr_data_filepath();
+ match File::open(&path) {
+ Ok(file) => {
+ let reader = BufReader::new(file);
+ from_reader(reader)
+ .with_context(|| format!("cannot read nostr data from '{}'", path.display()))
+ }
+ Err(_) => {
+ let nostr_data = NostrData::default();
+ let mut file = File::create(&path)?;
+ file.write_all(to_string(&nostr_data)?.as_bytes())
+ .with_context(|| format!("could not write nostr data to '{}'", path.display()))?;
+ Ok(nostr_data)
+ }
+ }
+}
+
+fn get_default_relays() -> Vec<String> {
+ vec![
+ "wss://nostr.bitcoiner.social",
+ "wss://nostr.oxtr.dev",
+ "wss://nostr.orangepill.dev",
+ "wss://relay.damus.io",
+ ]
+ .into_iter()
+ .map(String::from)
+ .collect()
+}
+
+fn get_default_bot_metadata() -> Metadata {
+ Metadata::new()
+ .name(formatcp!("{}bot", env!("CARGO_PKG_NAME")))
+ .display_name(formatcp!("{} bot", env!("CARGO_PKG_NAME")))
+ .about(env!("CARGO_PKG_DESCRIPTION"))
+ .website(env!("CARGO_PKG_REPOSITORY").parse().unwrap())
+ .picture("https://robohash.org/sentrumbot.png".parse().unwrap())
+ .banner(
+ "https://void.cat/d/HX1pPeqz21hvneLDibs5JD.webp"
+ .parse()
+ .unwrap(),
+ )
+ .lud06(formatcp!(
+ "https://sommerfeld.dev/.well-known/lnurlp/{}",
+ env!("CARGO_PKG_NAME")
+ ))
+ .lud16(formatcp!("{}@sommerfeld.dev", env!("CARGO_PKG_NAME")))
+}
+
+fn mark_bot_metadata_as_set(mut nostr_data: NostrData) -> Result<()> {
+ let path = get_nostr_data_filepath();
+ nostr_data.metadata_set = true;
+ let mut file = File::create(&path)?;
+ file.write_all(to_string(&nostr_data)?.as_bytes())
+ .with_context(|| format!("could not write nostr data to '{}'", path.display()))?;
+ Ok(())
+}
+
+#[derive(Deserialize, Debug)]
+pub struct NostrConfig {
+ #[serde(default = "get_default_relays")]
+ relays: Vec<String>,
+ proxy: Option<SocketAddr>,
+ #[serde(default = "get_default_bot_metadata")]
+ bot_metadata: Metadata,
+ #[serde(default)]
+ resend_bot_metadata: bool,
+ recipient: String,
+ #[serde(default)]
+ sealed_dm: bool,
+}
+
+impl NostrConfig {}
+
+pub struct NostrAction<'a> {
+ message_config: &'a MessageConfig,
+ client: Client,
+ recipient: PublicKey,
+ sealed_dm: bool,
+}
+
+impl<'a> NostrAction<'a> {
+ pub async fn new(
+ message_config: &'a MessageConfig,
+ nostr_config: &'a NostrConfig,
+ ) -> Result<Self> {
+ let nostr_data = get_nostr_data()?;
+ let keys = Keys::parse(&nostr_data.key)
+ .with_context(|| format!("could not parse nostr secret key '{}'", nostr_data.key))?;
+
+ let client = Client::new(&keys);
+
+ let relay_opts = RelayOptions::new().read(false).proxy(nostr_config.proxy);
+ TokioScope::scope_and_block(|s| {
+ for relay in nostr_config.relays.iter() {
+ s.spawn(client.add_relay_with_opts(relay.clone(), relay_opts.clone()));
+ }
+ });
+
+ client.connect().await;
+
+ if !nostr_data.metadata_set || nostr_config.resend_bot_metadata {
+ client.set_metadata(&nostr_config.bot_metadata).await?;
+ mark_bot_metadata_as_set(nostr_data)?;
+ }
+
+ let recipient = match PublicKey::parse(&nostr_config.recipient) {
+ Ok(p) => p,
+ Err(e) => {
+ nip05::get_profile(&nostr_config.recipient, nostr_config.proxy)
+ .await
+ .with_context(|| {
+ format!("invalid recipient '{}': {}", nostr_config.recipient, e)
+ })?
+ .public_key
+ }
+ };
+
+ Ok(Self {
+ message_config,
+ client,
+ recipient,
+ sealed_dm: nostr_config.sealed_dm,
+ })
+ }
+}
+
+#[async_trait]
+impl Action<'_> for NostrAction<'_> {
+ async fn run(&self, params: Option<&MessageParams<'_, '_>>) -> Result<()> {
+ let subject = self.message_config.subject(params)?;
+ let body = self.message_config.body(params)?;
+ let message = format!("{}\n{}", subject, body);
+
+ if self.sealed_dm {
+ self.client
+ .send_sealed_msg(self.recipient, message, None)
+ .await?;
+ } else {
+ self.client
+ .send_direct_msg(self.recipient, message, None)
+ .await?;
+ }
+ Ok(())
+ }
+}
diff --git a/src/actions/ntfy.rs b/src/actions/ntfy.rs
new file mode 100644
index 0000000..7d83b87
--- /dev/null
+++ b/src/actions/ntfy.rs
@@ -0,0 +1,119 @@
+use super::Action;
+use crate::message::MessageConfig;
+use crate::message::MessageFormat;
+use crate::message::MessageParams;
+use anyhow::Result;
+use async_trait::async_trait;
+use ntfy::Auth;
+use ntfy::Dispatcher;
+use ntfy::Payload;
+use ntfy::Priority;
+use ntfy::Url;
+use serde::Deserialize;
+
+#[derive(Deserialize, Debug)]
+#[serde(remote = "Priority")]
+#[serde(rename_all = "snake_case")]
+pub enum NtfyPriority {
+ Max = 5,
+ High = 4,
+ Default = 3,
+ Low = 2,
+ Min = 1,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct NtfyCredentials {
+ username: String,
+ password: String,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct NtfyConfig {
+ url: Option<String>,
+ proxy: Option<String>,
+ topic: Option<String>,
+ pub credentials: Option<NtfyCredentials>,
+ #[serde(with = "NtfyPriority")]
+ #[serde(default)]
+ pub priority: Priority,
+ pub tags: Option<Vec<String>>,
+ pub attach: Option<Url>,
+ pub filename: Option<String>,
+ pub delay: Option<String>,
+ pub email: Option<String>,
+}
+
+impl NtfyConfig {
+ pub fn url(&self) -> &str {
+ self.url.as_deref().unwrap_or("https://ntfy.sh")
+ }
+
+ pub fn topic(&self) -> &str {
+ self.topic.as_deref().unwrap_or(env!("CARGO_PKG_NAME"))
+ }
+}
+
+pub struct NtfyAction<'a> {
+ message_config: &'a MessageConfig,
+ dispatcher: Dispatcher,
+ payload_template: Payload,
+}
+
+impl<'a> NtfyAction<'a> {
+ pub fn new(message_config: &'a MessageConfig, ntfy_config: &'a NtfyConfig) -> Result<Self> {
+ let mut dispatcher_builder = Dispatcher::builder(ntfy_config.url());
+ if let Some(cred) = &ntfy_config.credentials {
+ dispatcher_builder =
+ dispatcher_builder.credentials(Auth::new(&cred.username, &cred.password));
+ }
+ if let Some(proxy) = &ntfy_config.proxy {
+ dispatcher_builder = dispatcher_builder.proxy(proxy);
+ }
+
+ let mut payload = Payload::new(ntfy_config.topic())
+ .markdown(match message_config.format() {
+ MessageFormat::Plain => false,
+ MessageFormat::Markdown => true,
+ MessageFormat::Html => true,
+ })
+ .priority(ntfy_config.priority.clone())
+ .tags(
+ ntfy_config
+ .tags
+ .as_deref()
+ .unwrap_or(&["rotating_light".to_string()]),
+ );
+ if let Some(attach) = &ntfy_config.attach {
+ payload = payload.attach(attach.clone());
+ }
+ if let Some(filename) = &ntfy_config.filename {
+ payload = payload.filename(filename.clone());
+ }
+ if let Some(delay) = &ntfy_config.delay {
+ payload = payload.delay(delay.parse()?);
+ }
+ if let Some(email) = &ntfy_config.email {
+ payload = payload.email(email.clone());
+ }
+ Ok(Self {
+ message_config,
+ dispatcher: dispatcher_builder.build()?,
+ payload_template: payload,
+ })
+ }
+}
+
+#[async_trait]
+impl Action<'_> for NtfyAction<'_> {
+ async fn run(&self, params: Option<&MessageParams<'_, '_>>) -> Result<()> {
+ let payload = self
+ .payload_template
+ .clone()
+ .title(self.message_config.subject(params)?)
+ .message(self.message_config.body(params)?)
+ .click(self.message_config.get_tx_url(params)?.parse()?);
+ self.dispatcher.send(&payload).await?;
+ Ok(())
+ }
+}
diff --git a/src/actions/telegram.rs b/src/actions/telegram.rs
new file mode 100644
index 0000000..b489864
--- /dev/null
+++ b/src/actions/telegram.rs
@@ -0,0 +1,56 @@
+use super::Action;
+use crate::message::MessageConfig;
+use crate::message::MessageParams;
+use anyhow::Result;
+use async_trait::async_trait;
+use serde::Deserialize;
+use teloxide::requests::Requester;
+use teloxide::types::UserId;
+use teloxide::Bot;
+
+#[derive(Deserialize, Debug)]
+pub struct TelegramConfig {
+ bot_token: String,
+ user_id: u64,
+}
+
+impl TelegramConfig {
+ pub fn bot_token(&self) -> &str {
+ &self.bot_token
+ }
+
+ pub fn user_id(&self) -> u64 {
+ self.user_id
+ }
+}
+
+pub struct TelegramAction<'a> {
+ message_config: &'a MessageConfig,
+ bot: Bot,
+ user_id: UserId,
+}
+
+impl<'a> TelegramAction<'a> {
+ pub fn new(
+ message_config: &'a MessageConfig,
+ telegram_config: &'a TelegramConfig,
+ ) -> Result<Self> {
+ Ok(Self {
+ message_config,
+ bot: Bot::new(telegram_config.bot_token()),
+ user_id: UserId(telegram_config.user_id()),
+ })
+ }
+}
+
+#[async_trait]
+impl Action<'_> for TelegramAction<'_> {
+ async fn run(&self, params: Option<&MessageParams<'_, '_>>) -> Result<()> {
+ let subject = self.message_config.subject(params)?;
+ let body = self.message_config.body(params)?;
+ self.bot
+ .send_message(self.user_id, format!("{}\n{}", subject, body))
+ .await?;
+ Ok(())
+ }
+}
diff --git a/src/actions/terminal_print.rs b/src/actions/terminal_print.rs
new file mode 100644
index 0000000..02536c7
--- /dev/null
+++ b/src/actions/terminal_print.rs
@@ -0,0 +1,28 @@
+use super::Action;
+use crate::message::MessageConfig;
+use crate::message::MessageParams;
+use anyhow::Result;
+use async_trait::async_trait;
+
+#[derive(Debug)]
+pub struct TerminalPrintAction<'a> {
+ message_config: &'a MessageConfig,
+}
+
+impl<'a> TerminalPrintAction<'a> {
+ pub fn new(message_config: &'a MessageConfig) -> Self {
+ Self { message_config }
+ }
+}
+
+#[async_trait]
+impl Action<'_> for TerminalPrintAction<'_> {
+ async fn run(&self, params: Option<&MessageParams<'_, '_>>) -> Result<()> {
+ println!(
+ "{}\n{}\n",
+ self.message_config.subject(params)?,
+ self.message_config.body(params)?
+ );
+ Ok(())
+ }
+}
diff --git a/src/blockchain.rs b/src/blockchain.rs
new file mode 100644
index 0000000..2f5014b
--- /dev/null
+++ b/src/blockchain.rs
@@ -0,0 +1,96 @@
+use anyhow::{Context, Result};
+use bdk::{
+ bitcoin::Network,
+ blockchain::{ElectrumBlockchain, GetHeight},
+ electrum_client::{Client, ConfigBuilder, Socks5Config},
+};
+use serde::Deserialize;
+
+fn get_default_electrum_server(network: Network) -> &'static str {
+ match network {
+ Network::Bitcoin => "ssl://fulcrum.sethforprivacy.com:50002",
+ Network::Testnet => "ssl://electrum.blockstream.info:60002",
+ Network::Signet => "ssl://mempool.space:60602",
+ _ => panic!("unsupported network"),
+ }
+}
+
+#[derive(Deserialize, Default, Debug)]
+pub struct ElectrumConfig {
+ url: Option<String>,
+
+ network: Option<Network>,
+
+ socks5: Option<String>,
+
+ #[serde(default)]
+ certificate_validation: bool,
+}
+
+impl ElectrumConfig {
+ pub fn url(&self) -> &str {
+ self.url
+ .as_deref()
+ .unwrap_or(get_default_electrum_server(self.network()))
+ }
+
+ pub fn network(&self) -> Network {
+ self.network.unwrap_or(Network::Bitcoin)
+ }
+
+ pub fn certificate_validation(&self) -> bool {
+ self.certificate_validation
+ }
+
+ pub fn socks5(&self) -> Option<Socks5Config> {
+ self.socks5.as_ref().map(Socks5Config::new)
+ }
+}
+
+pub fn get_blockchain(electrum_cfg: &ElectrumConfig) -> Result<ElectrumBlockchain> {
+ let server_cfg = ConfigBuilder::new()
+ .validate_domain(electrum_cfg.certificate_validation())
+ .socks5(electrum_cfg.socks5())
+ .build();
+ let electrum_url = electrum_cfg.url();
+ let client = Client::from_config(electrum_url, server_cfg)
+ .with_context(|| "could not configure electrum client".to_string())?;
+ Ok(ElectrumBlockchain::from(client))
+}
+
+pub struct BlockchainState {
+ height: Option<u32>,
+ url: String,
+ blockchain: ElectrumBlockchain,
+}
+
+impl BlockchainState {
+ pub fn new(electrum_cfg: &ElectrumConfig) -> Result<Self> {
+ Ok(Self {
+ height: Default::default(),
+ url: String::from(electrum_cfg.url()),
+ blockchain: get_blockchain(electrum_cfg)?,
+ })
+ }
+
+ pub fn update_height(&mut self) {
+ match self.blockchain.get_height() {
+ Ok(polled_height) => {
+ match self.height {
+ Some(h) => {
+ if polled_height != h {
+ self.height = Some(polled_height);
+ debug!("current block height: {}", polled_height);
+ }
+ }
+ None => {
+ self.height = Some(polled_height);
+ info!("connected to '{}'", self.url);
+ info!("current block height: {}", polled_height);
+ }
+ };
+ }
+ Err(e) => warn!("could not reach '{}': {}", self.url, e),
+ };
+ }
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..2f3e5ed
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,143 @@
+use std::{
+ env, fs,
+ path::{Path, PathBuf},
+};
+
+use anyhow::{bail, Context, Result};
+use clap::Parser;
+use const_format::{formatcp, map_ascii_case, Case};
+use serde::Deserialize;
+
+use crate::{
+ actions::AnyActionConfig, blockchain::ElectrumConfig, message::MessageConfig,
+ wallets::WalletConfig,
+};
+
+#[derive(Parser, Debug)]
+#[command(version, about)]
+pub struct Args {
+ /// Path to toml configuration file
+ config: Option<String>,
+ /// Perform configured actions on a test notification
+ #[arg(short, long)]
+ test: bool,
+ /// Notify for every past transaction (careful: if you have a long transaction history, this
+ /// can SPAM your configured actions
+ #[arg(short, long)]
+ notify_past_txs: bool,
+}
+
+impl Args {
+ pub fn config(&self) -> Option<&str> {
+ self.config.as_deref()
+ }
+
+ pub fn test(&self) -> bool {
+ self.test
+ }
+
+ pub fn notify_past_txs(&self) -> bool {
+ self.notify_past_txs
+ }
+}
+
+fn get_config_filename() -> &'static str {
+ formatcp!("{}.toml", env!("CARGO_PKG_NAME"))
+}
+
+fn get_config_env_var() -> &'static str {
+ formatcp!(
+ "{}_CONFIG",
+ map_ascii_case!(Case::Upper, env!("CARGO_PKG_NAME"))
+ )
+}
+
+fn get_cwd_config_path() -> PathBuf {
+ PathBuf::from(".").join(get_config_filename())
+}
+
+fn get_config_path_impl(user_config_dir: &Path) -> PathBuf {
+ user_config_dir
+ .join(env!("CARGO_PKG_NAME"))
+ .join(get_config_filename())
+}
+
+fn get_user_config_path() -> Option<PathBuf> {
+ dirs::config_dir().map(|p| get_config_path_impl(&p))
+}
+
+fn get_system_config_path() -> PathBuf {
+ get_config_path_impl(&systemd_directories::config_dir().unwrap_or(PathBuf::from("/etc")))
+}
+
+fn get_config_path(maybe_arg_config: &Option<&str>) -> Result<PathBuf> {
+ if let Some(arg_path) = maybe_arg_config {
+ return Ok(PathBuf::from(arg_path));
+ }
+
+ if let Ok(env_path) = env::var(get_config_env_var()) {
+ return Ok(PathBuf::from(env_path));
+ }
+
+ let cwd_config_path = get_cwd_config_path();
+ if cwd_config_path.try_exists().is_ok_and(|x| x) {
+ return Ok(cwd_config_path);
+ }
+
+ if let Some(user_config_path) = get_user_config_path() {
+ if user_config_path.try_exists().is_ok_and(|x| x) {
+ return Ok(user_config_path);
+ }
+ }
+
+ let system_config_path = get_system_config_path();
+ if system_config_path.try_exists().is_ok_and(|x| x) {
+ return Ok(system_config_path);
+ }
+
+ bail!(
+ "no configuration file was passed as first argument, nor by the '{}' environment variable, nor did one exist in the default search paths: '{}', '{}', '{}'",
+ get_config_env_var(),
+ get_cwd_config_path().display(),
+ get_user_config_path().unwrap_or_default().display(),
+ get_system_config_path().display()
+ );
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Config {
+ wallets: Vec<WalletConfig>,
+ #[serde(default)]
+ electrum: ElectrumConfig,
+ #[serde(default)]
+ message: MessageConfig,
+ #[serde(default)]
+ actions: Vec<AnyActionConfig>,
+}
+
+impl Config {
+ pub fn electrum(&self) -> &ElectrumConfig {
+ &self.electrum
+ }
+
+ pub fn wallets(&self) -> &[WalletConfig] {
+ &self.wallets
+ }
+
+ pub fn message(&self) -> &MessageConfig {
+ &self.message
+ }
+
+ pub fn actions(&self) -> &[AnyActionConfig] {
+ &self.actions
+ }
+}
+
+pub fn get_config(maybe_arg_config: &Option<&str>) -> Result<Config> {
+ let config_path = get_config_path(maybe_arg_config)?;
+ info!("reading configuration from '{}'", config_path.display());
+ let config_content = fs::read_to_string(&config_path)
+ .with_context(|| format!("could not read config file '{}'", config_path.display()))?;
+ toml::from_str(&config_content)
+ .with_context(|| format!("could not parse config file '{}'", config_path.display(),))
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..9b10935
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,179 @@
+extern crate pretty_env_logger;
+#[macro_use]
+extern crate log;
+
+use std::process::exit;
+use std::time::Duration;
+
+use actions::Action;
+use async_scoped::TokioScope;
+use clap::Parser;
+use human_panic::setup_panic;
+
+use anyhow::{bail, Context, Result};
+use tokio::signal::unix::{signal, SignalKind};
+use tokio::time::sleep;
+
+mod actions;
+mod blockchain;
+mod config;
+mod message;
+mod wallets;
+
+use crate::actions::get_actions;
+use crate::message::MessageParams;
+use crate::{
+ blockchain::BlockchainState,
+ config::{get_config, Args},
+ wallets::{get_wallets, SafeWalletInfo},
+};
+
+fn set_logger() {
+ pretty_env_logger::formatted_builder()
+ .filter_module(env!("CARGO_PKG_NAME"), log::LevelFilter::Info)
+ .parse_default_env()
+ .init();
+}
+
+fn set_signal_handlers() -> Result<()> {
+ tokio::spawn(async move {
+ if let Err(e) = tokio::signal::ctrl_c().await {
+ return e;
+ }
+ warn!("received ctrl-c signal. Exiting...");
+ exit(0)
+ });
+ tokio::spawn(async move {
+ let mut stream = match signal(SignalKind::terminate()) {
+ Err(e) => return e,
+ Ok(s) => s,
+ };
+ stream.recv().await;
+ warn!("received process termination signal. Exiting...");
+ exit(0)
+ });
+ tokio::spawn(async move {
+ let mut stream = match signal(SignalKind::hangup()) {
+ Err(e) => return e,
+ Ok(s) => s,
+ };
+ stream.recv().await;
+ warn!("received process hangup signal. Exiting...");
+ exit(0)
+ });
+ Ok(())
+}
+
+async fn run_test_actions(actions: &[&(dyn Action<'_> + Sync)]) {
+ TokioScope::scope_and_block(|s| {
+ for &action in actions {
+ s.spawn(action.run(None));
+ }
+ });
+}
+
+fn get_and_handle_new_txs(
+ wallet_info: &SafeWalletInfo,
+ actions: &[&(dyn Action<'_> + Sync)],
+) -> Result<()> {
+ let mut locked_wallet_info = wallet_info.lock().unwrap();
+ let txs = locked_wallet_info.get_new_txs();
+ TokioScope::scope_and_block(|s| {
+ for tx in txs.iter() {
+ let params = MessageParams::new(tx, &locked_wallet_info);
+ s.spawn(async move {
+ TokioScope::scope_and_block(|s| {
+ for &action in actions {
+ s.spawn(action.run(Some(&params)));
+ }
+ });
+ });
+ }
+ });
+ Ok(())
+}
+
+async fn update_blockchain_thread(blockchain_state: &mut BlockchainState) {
+ loop {
+ blockchain_state.update_height();
+ sleep(Duration::from_secs(60)).await;
+ }
+}
+
+async fn watch_wallet_thread(wallet_info: &SafeWalletInfo, actions: &[&(dyn Action<'_> + Sync)]) {
+ loop {
+ if let Err(e) = get_and_handle_new_txs(wallet_info, actions) {
+ warn!("{:?}", e);
+ }
+ }
+}
+
+async fn initial_wallet_sync(blockchain_state: &mut BlockchainState, wallets: &[SafeWalletInfo]) {
+ TokioScope::scope_and_block(|s| {
+ s.spawn(async { blockchain_state.update_height() });
+ for wallet_info in wallets {
+ s.spawn(async {
+ if let Err(e) = get_and_handle_new_txs(wallet_info, &[]) {
+ warn!("{:?}", e);
+ }
+ });
+ }
+ });
+}
+
+async fn watch_wallets(
+ blockchain_state: &mut BlockchainState,
+ wallets: &[SafeWalletInfo],
+ actions: &[&(dyn Action<'_> + Sync)],
+) {
+ TokioScope::scope_and_block(|s| {
+ s.spawn(update_blockchain_thread(blockchain_state));
+ for wallet_info in wallets {
+ s.spawn(watch_wallet_thread(wallet_info, actions));
+ }
+ });
+}
+
+async fn do_main() -> Result<()> {
+ setup_panic!();
+ let args = Args::parse();
+ set_logger();
+ set_signal_handlers().context("failed to setup a signal termination handler")?;
+
+ let config = get_config(&args.config())?;
+
+ let actions = get_actions(config.message(), config.actions()).await;
+ if actions.is_empty() {
+ bail!("no actions properly configured");
+ }
+ let actions_ref = actions.iter().map(Box::as_ref).collect::<Vec<_>>();
+
+ if args.test() {
+ run_test_actions(&actions_ref).await;
+ return Ok(());
+ }
+
+ let mut blockchain_state = BlockchainState::new(config.electrum())?;
+
+ let wallets = get_wallets(config.wallets(), config.electrum());
+ if wallets.is_empty() {
+ bail!("no wallets properly configured");
+ }
+
+ if !args.notify_past_txs() {
+ info!("initial wallet sync");
+ initial_wallet_sync(&mut blockchain_state, &wallets).await;
+ }
+ info!("listening for new relevant events");
+ watch_wallets(&mut blockchain_state, &wallets, &actions_ref).await;
+
+ Ok(())
+}
+
+#[tokio::main]
+async fn main() {
+ if let Err(e) = do_main().await {
+ error!("{:?}", e);
+ exit(1);
+ }
+}
diff --git a/src/message.rs b/src/message.rs
new file mode 100644
index 0000000..419bb4d
--- /dev/null
+++ b/src/message.rs
@@ -0,0 +1,203 @@
+extern crate chrono;
+extern crate strfmt;
+
+use anyhow::{bail, Context, Result};
+use bdk::{bitcoin::Network, TransactionDetails};
+use chrono::DateTime;
+use serde::Deserialize;
+use strfmt::strfmt;
+
+use crate::wallets::WalletInfo;
+
+pub struct MessageParams<'a, 'b> {
+ tx: &'a TransactionDetails,
+ wallet: &'b str,
+ total_balance: u64,
+ current_height: u32,
+ network: Network,
+}
+
+impl<'a, 'b> MessageParams<'a, 'b> {
+ pub fn new(tx: &'a TransactionDetails, wallet: &'b WalletInfo) -> Self {
+ Self {
+ tx,
+ wallet: wallet.name(),
+ total_balance: wallet.total_balance().unwrap_or_default(),
+ current_height: wallet.get_height().unwrap_or_default(),
+ network: wallet.get_network(),
+ }
+ }
+
+ fn tx_net(&self) -> i64 {
+ (self.tx.received as i64) - (self.tx.sent as i64)
+ }
+
+ fn tx_height(&self) -> Option<u32> {
+ self.tx.confirmation_time.as_ref().map(|x| x.height)
+ }
+
+ fn confs(&self) -> u32 {
+ let current_height = self.current_height;
+ self.tx_height()
+ .map(|h| {
+ if current_height >= h {
+ current_height - h
+ } else {
+ 0
+ }
+ })
+ .unwrap_or_default()
+ }
+
+ fn conf_timestamp(&self) -> String {
+ self.tx
+ .confirmation_time
+ .as_ref()
+ .map(|x| {
+ DateTime::from_timestamp(x.timestamp as i64, 0)
+ .unwrap_or_default()
+ .format("%Y-%m-%d %H:%M:%S UTC")
+ .to_string()
+ })
+ .unwrap_or_default()
+ }
+
+ fn txid(&self) -> String {
+ self.tx.txid.to_string()
+ }
+ fn txid_short(&self) -> String {
+ let txid = self.txid();
+ format!("{}...{}", &txid[..6], &txid[txid.len() - 6..])
+ }
+
+ fn tx(&self) -> &TransactionDetails {
+ self.tx
+ }
+
+ pub fn network(&self) -> Network {
+ self.network
+ }
+}
+
+#[derive(Deserialize, Debug, PartialEq, Copy, Clone)]
+pub enum MessageFormat {
+ Plain,
+ Markdown,
+ Html,
+}
+
+#[derive(Deserialize, Default, Debug)]
+pub struct BlockExplorers {
+ mainnet: Option<String>,
+ testnet: Option<String>,
+ signet: Option<String>,
+}
+
+impl BlockExplorers {
+ fn mainnet(&self) -> &str {
+ self.mainnet
+ .as_deref()
+ .unwrap_or("https://mempool.space/tx/{txid}")
+ }
+
+ fn testnet(&self) -> &str {
+ self.testnet
+ .as_deref()
+ .unwrap_or("https://mempool.space/testnet/tx/{txid}")
+ }
+
+ fn signet(&self) -> &str {
+ self.signet
+ .as_deref()
+ .unwrap_or("https://mempool.space/signet/tx/{txid}")
+ }
+
+ pub fn get_tx_url_template(&self, network: &Network) -> Result<&str> {
+ Ok(match network {
+ Network::Bitcoin => self.mainnet(),
+ Network::Testnet => self.testnet(),
+ Network::Signet => self.signet(),
+ _ => bail!("unsupported network"),
+ })
+ }
+ pub fn get_tx_url(&self, network: &Network, txid: &str) -> Result<String> {
+ let template = self.get_tx_url_template(network)?;
+ strfmt!(template, txid => txid.to_string())
+ .with_context(|| format!("bad block explorer URL template '{}'", template))
+ }
+}
+
+#[derive(Deserialize, Default, Debug)]
+pub struct MessageConfig {
+ subject: Option<String>,
+ body: Option<String>,
+ format: Option<MessageFormat>,
+ #[serde(default)]
+ block_explorers: BlockExplorers,
+}
+
+impl MessageConfig {
+ pub fn subject_template(&self) -> &str {
+ self.subject
+ .as_deref()
+ .unwrap_or("[{wallet}] new transaction")
+ }
+
+ pub fn body_template(&self) -> &str {
+ self.body
+ .as_deref()
+ .unwrap_or("net: {tx_net} sats, balance: {total_balance} sats, txid: {txid_short}")
+ }
+
+ pub fn replace_template_params(
+ &self,
+ template: &str,
+ params: &MessageParams,
+ ) -> Result<String> {
+ strfmt!(template,
+ tx_net => params.tx_net(),
+ wallet => params.wallet.to_string(),
+ total_balance => params.total_balance,
+ txid => params.txid(),
+ txid_short => params.txid_short(),
+ received => params.tx().received,
+ sent => params.tx().sent,
+ fee => params.tx().fee.unwrap_or_default(),
+ current_height => params.current_height,
+ tx_height => params.tx_height().unwrap_or_default(),
+ confs => params.confs(),
+ conf_timestamp => params.conf_timestamp(),
+ tx_url => self.get_tx_url(Some(params))?
+ )
+ .with_context(|| format!("invalid template '{}'", template))
+ }
+
+ pub fn subject(&self, params: Option<&MessageParams>) -> Result<String> {
+ match params {
+ Some(p) => self.replace_template_params(self.subject_template(), p),
+ None => Ok(self.subject_template().to_string()),
+ }
+ }
+
+ pub fn body(&self, params: Option<&MessageParams>) -> Result<String> {
+ match params {
+ Some(p) => self.replace_template_params(self.body_template(), p),
+ None => Ok(self.body_template().to_string()),
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn format(&self) -> &MessageFormat {
+ self.format.as_ref().unwrap_or(&MessageFormat::Plain)
+ }
+
+ pub fn get_tx_url(&self, params: Option<&MessageParams>) -> Result<String> {
+ match params {
+ Some(p) => self.block_explorers.get_tx_url(&p.network(), &p.txid()),
+ None => Ok(self
+ .block_explorers
+ .get_tx_url_template(&Network::Bitcoin)?
+ .to_string()),
+ }
+ }
+}
diff --git a/src/wallets.rs b/src/wallets.rs
new file mode 100644
index 0000000..ca43e0a
--- /dev/null
+++ b/src/wallets.rs
@@ -0,0 +1,230 @@
+use std::{
+ collections::{hash_map::DefaultHasher, HashSet},
+ hash::{Hash, Hasher},
+ path::PathBuf,
+ sync::{Arc, Mutex},
+};
+
+use anyhow::{Context, Result};
+use bdk::{
+ bitcoin::{bip32::ExtendedPubKey, Network, Txid},
+ blockchain::{ElectrumBlockchain, GetHeight},
+ sled,
+ template::{Bip44Public, Bip49Public, Bip84Public, Bip86Public},
+ KeychainKind, SyncOptions, TransactionDetails, Wallet,
+};
+use serde::Deserialize;
+
+use crate::blockchain::{get_blockchain, ElectrumConfig};
+
+#[derive(Deserialize, Debug, Clone, Copy)]
+#[serde(rename_all = "snake_case")]
+pub enum AddressKind {
+ Legacy,
+ NestedSegwit,
+ Segwit,
+ Taproot,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct XpubSpec {
+ name: String,
+ xpub: String,
+ kind: Option<AddressKind>,
+}
+
+impl XpubSpec {
+ pub fn kind(&self) -> AddressKind {
+ self.kind.unwrap_or(AddressKind::Segwit)
+ }
+
+ pub fn xpub(&self) -> &str {
+ &self.xpub
+ }
+
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+}
+
+#[derive(Deserialize, Debug, Hash)]
+pub struct DescriptorsSpec {
+ name: String,
+ primary: String,
+ change: Option<String>,
+}
+
+impl DescriptorsSpec {
+ pub fn get_hash(&self) -> String {
+ let mut s = DefaultHasher::new();
+ self.hash(&mut s);
+ s.finish().to_string()
+ }
+
+ pub fn primary(&self) -> &str {
+ &self.primary
+ }
+
+ pub fn change(&self) -> Option<&String> {
+ self.change.as_ref()
+ }
+
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(untagged)]
+pub enum WalletConfig {
+ Xpub(XpubSpec),
+ Descriptors(DescriptorsSpec),
+}
+
+impl WalletConfig {
+ pub fn name(&self) -> &str {
+ match self {
+ WalletConfig::Xpub(xpub_spec) => xpub_spec.name(),
+ WalletConfig::Descriptors(descriptors_spec) => descriptors_spec.name(),
+ }
+ }
+}
+
+fn get_cache_dir(db_name: &str) -> PathBuf {
+ dirs::cache_dir()
+ .unwrap_or(PathBuf::from("cache"))
+ .join(env!("CARGO_PKG_NAME"))
+ .join(db_name)
+}
+
+fn get_xpub_wallet(xpub_spec: &XpubSpec, network: Network) -> Result<Wallet<sled::Tree>> {
+ let xpub: ExtendedPubKey = xpub_spec.xpub().parse().unwrap();
+ let fingerprint = xpub.fingerprint();
+ let sled = sled::open(get_cache_dir(&fingerprint.to_string()))?.open_tree("wallet")?;
+ match xpub_spec.kind() {
+ AddressKind::Legacy => Wallet::new(
+ Bip44Public(xpub, fingerprint, KeychainKind::External),
+ Some(Bip44Public(xpub, fingerprint, KeychainKind::Internal)),
+ network,
+ sled,
+ ),
+ AddressKind::NestedSegwit => Wallet::new(
+ Bip49Public(xpub, fingerprint, KeychainKind::External),
+ Some(Bip49Public(xpub, fingerprint, KeychainKind::Internal)),
+ network,
+ sled,
+ ),
+ AddressKind::Segwit => Wallet::new(
+ Bip84Public(xpub, fingerprint, KeychainKind::External),
+ Some(Bip84Public(xpub, fingerprint, KeychainKind::Internal)),
+ network,
+ sled,
+ ),
+ AddressKind::Taproot => Wallet::new(
+ Bip86Public(xpub, fingerprint, KeychainKind::External),
+ Some(Bip86Public(xpub, fingerprint, KeychainKind::Internal)),
+ network,
+ sled,
+ ),
+ }
+ .with_context(|| format!("invalid xpub wallet '{}'", xpub))
+}
+
+fn get_descriptors_wallet(
+ descriptors_spec: &DescriptorsSpec,
+ network: Network,
+) -> Result<Wallet<sled::Tree>> {
+ let sled = sled::open(get_cache_dir(&descriptors_spec.get_hash()))?.open_tree("wallet")?;
+ Wallet::new(
+ descriptors_spec.primary(),
+ descriptors_spec.change().map(String::as_ref),
+ network,
+ sled,
+ )
+ .with_context(|| format!("invalid descriptor wallet '{:?}'", descriptors_spec))
+}
+
+fn get_wallet(wallet_config: &WalletConfig, network: Network) -> Result<Wallet<sled::Tree>> {
+ match &wallet_config {
+ WalletConfig::Xpub(xpub_spec) => get_xpub_wallet(xpub_spec, network),
+ WalletConfig::Descriptors(descriptors_spec) => {
+ get_descriptors_wallet(descriptors_spec, network)
+ }
+ }
+}
+
+pub struct WalletInfo {
+ name: String,
+ wallet: Wallet<sled::Tree>,
+ old_txs: HashSet<Txid>,
+ blockchain: ElectrumBlockchain,
+}
+
+pub type SafeWalletInfo = Arc<Mutex<WalletInfo>>;
+
+impl WalletInfo {
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ pub fn get_height(&self) -> Result<u32, bdk::Error> {
+ self.blockchain.get_height()
+ }
+
+ pub fn get_network(&self) -> Network {
+ self.wallet.network()
+ }
+
+ pub fn total_balance(&self) -> Result<u64, bdk::Error> {
+ self.wallet.get_balance().map(|b| b.get_total())
+ }
+
+ pub fn get_new_txs(&mut self) -> Vec<TransactionDetails> {
+ debug!("[{}] syncing wallet", self.name);
+ if let Err(e) = self.wallet.sync(&self.blockchain, SyncOptions::default()) {
+ warn!("[{}] cannot sync wallet: {}", self.name, e);
+ return Default::default();
+ }
+ let tx_list = match self.wallet.list_transactions(false) {
+ Ok(txs) => txs,
+ Err(e) => {
+ warn!("[{}] cannot retrieve transactions: {}", self.name, e);
+ Default::default()
+ }
+ };
+
+ let new_txs: Vec<TransactionDetails> = tx_list
+ .iter()
+ .filter(|&tx| !self.old_txs.contains(&tx.txid))
+ .cloned()
+ .collect();
+ new_txs.iter().for_each(|tx| {
+ self.old_txs.insert(tx.txid);
+ });
+ new_txs
+ }
+}
+
+pub fn get_wallets(
+ wallet_configs: &[WalletConfig],
+ electrum_cfg: &ElectrumConfig,
+) -> Vec<SafeWalletInfo> {
+ let mut result: Vec<SafeWalletInfo> = vec![];
+ for wallet_config in wallet_configs.iter() {
+ let name = wallet_config.name();
+ match get_wallet(wallet_config, electrum_cfg.network()) {
+ Ok(w) => {
+ result.push(Arc::new(Mutex::new(WalletInfo {
+ name: name.to_string(),
+ wallet: w,
+ old_txs: Default::default(),
+ blockchain: get_blockchain(electrum_cfg).unwrap(),
+ })));
+ }
+ Err(e) => {
+ error!("[{}] cannot setup wallet: {}", name, e);
+ }
+ }
+ }
+ result
+}