From a573b6e5adee497473461141648bf58f5e97413f Mon Sep 17 00:00:00 2001
From: parabit
Date: Sun, 15 Feb 2026 18:19:39 -0600
Subject: [PATCH 1/7] initial code structuring
---
.gitignore | 4 +-
Cargo.lock | 668 ++++++++++++++++++++++++++++++++++++++-
Cargo.toml | 17 +
README.md | 34 +-
bin/.gitignore | 4 +
bin/.just-1.46.0.pkg | 1 -
bin/.rustup-1.28.2.pkg | 1 -
bin/README.hermit.md | 7 -
bin/activate-hermit | 21 --
bin/activate-hermit.fish | 24 --
bin/cargo | 1 -
bin/cargo-clippy | 1 -
bin/cargo-fmt | 1 -
bin/cargo-miri | 1 -
bin/clippy-driver | 1 -
bin/hermit | 43 ---
bin/hermit.hcl | 2 -
bin/just | 1 -
bin/rls | 1 -
bin/rust-analyzer | 1 -
bin/rust-gdb | 1 -
bin/rust-gdbgui | 1 -
bin/rust-lldb | 1 -
bin/rustc | 1 -
bin/rustdoc | 1 -
bin/rustfmt | 1 -
bin/rustup | 1 -
justfile | 2 +
src/api.rs | 48 +++
src/bin/main.rs | 5 +-
src/lib.rs | 45 ++-
src/server.rs | 34 ++
src/swap.rs | 130 ++++++++
tests/api.rs | 70 ++++
tests/lib.rs | 55 ++++
tests/server.rs | 38 +++
tests/swap.rs | 121 +++++++
37 files changed, 1264 insertions(+), 125 deletions(-)
create mode 100644 bin/.gitignore
delete mode 120000 bin/.just-1.46.0.pkg
delete mode 120000 bin/.rustup-1.28.2.pkg
delete mode 100644 bin/README.hermit.md
delete mode 100755 bin/activate-hermit
delete mode 100755 bin/activate-hermit.fish
delete mode 120000 bin/cargo
delete mode 120000 bin/cargo-clippy
delete mode 120000 bin/cargo-fmt
delete mode 120000 bin/cargo-miri
delete mode 120000 bin/clippy-driver
delete mode 100755 bin/hermit
delete mode 100644 bin/hermit.hcl
delete mode 120000 bin/just
delete mode 120000 bin/rls
delete mode 120000 bin/rust-analyzer
delete mode 120000 bin/rust-gdb
delete mode 120000 bin/rust-gdbgui
delete mode 120000 bin/rust-lldb
delete mode 120000 bin/rustc
delete mode 120000 bin/rustdoc
delete mode 120000 bin/rustfmt
delete mode 120000 bin/rustup
create mode 100644 src/api.rs
create mode 100644 src/server.rs
create mode 100644 src/swap.rs
create mode 100644 tests/api.rs
create mode 100644 tests/server.rs
create mode 100644 tests/swap.rs
diff --git a/.gitignore b/.gitignore
index a2f9ba9..9d49729 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
/target
/.hermit
-/.idea
\ No newline at end of file
+/.idea
+
+justfile.local
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 11d7c2d..dffceae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -38,7 +38,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -49,7 +49,164 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
- "windows-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "axum"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base58ck"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
+dependencies = [
+ "bitcoin-internals",
+ "bitcoin_hashes",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "bech32"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
+
+[[package]]
+name = "bitcoin"
+version = "0.32.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66"
+dependencies = [
+ "base58ck",
+ "base64",
+ "bech32",
+ "bitcoin-internals",
+ "bitcoin-io",
+ "bitcoin-units",
+ "bitcoin_hashes",
+ "hex-conservative",
+ "hex_lit",
+ "secp256k1",
+ "serde",
+]
+
+[[package]]
+name = "bitcoin-internals"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitcoin-io"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
+
+[[package]]
+name = "bitcoin-units"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2"
+dependencies = [
+ "bitcoin-internals",
+ "serde",
+]
+
+[[package]]
+name = "bitcoin_hashes"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
+dependencies = [
+ "bitcoin-io",
+ "hex-conservative",
+ "serde",
+]
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cc"
+version = "1.2.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
]
[[package]]
@@ -102,7 +259,62 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
name = "entangle"
version = "0.0.1"
dependencies = [
+ "axum",
+ "bitcoin",
"clap",
+ "http-body-util",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tower",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
]
[[package]]
@@ -111,18 +323,185 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "hex-conservative"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "hex_lit"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "bytes",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "libc"
+version = "0.2.182"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mio"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -141,6 +520,126 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "secp256k1"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
+dependencies = [
+ "bitcoin_hashes",
+ "secp256k1-sys",
+ "serde",
+]
+
+[[package]]
+name = "secp256k1-sys"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[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 = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
[[package]]
name = "strsim"
version = "0.11.1"
@@ -158,6 +657,85 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
+[[package]]
+name = "tokio"
+version = "1.49.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
+dependencies = [
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.22"
@@ -170,12 +748,27 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets",
+]
+
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -184,3 +777,74 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
index 013401e..391d840 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ name = "entangle"
description = "Swaps made easy"
version = "0.0.1"
homepage = "https://github.com/parasitepool/entangle"
+autotests = false
authors.workspace = true
edition.workspace = true
@@ -21,10 +22,26 @@ repository = "https://github.com/parasitepool/entangle"
rust-version = "1.91.0"
[workspace.dependencies]
+axum = "0.8"
+bitcoin = { version = "0.32", features = ["base64", "serde", "std"] }
clap = { version = "4.5.36", features = ["derive", "env"] }
+http-body-util = "0.1"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+tower = { version = "0.5", features = ["util"] }
[dependencies]
+axum.workspace = true
+bitcoin.workspace = true
clap.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+tokio.workspace = true
+
+[dev-dependencies]
+http-body-util.workspace = true
+tower.workspace = true
[[bin]]
name = "entangle"
diff --git a/README.md b/README.md
index 5d8db2a..161ce6a 100644
--- a/README.md
+++ b/README.md
@@ -13,4 +13,36 @@
-`entangle` is currently in early development check back soon.
\ No newline at end of file
+`entangle` builds Bitcoin swap transactions. Given two UTXOs owned by different parties, it produces a PSBT that safely exchanges them — party A signs, then party B signs, and the swap is complete.
+
+## Usage
+
+As a **library**:
+
+```rust
+use entangle::swap::{build_swap_psbt, SwapRequest};
+
+let psbt = build_swap_psbt(&request)?;
+```
+
+As an **API server**:
+
+```sh
+entangle api --bind 0.0.0.0:3000
+```
+
+As a **frontend server** (with optional embedded API):
+
+```sh
+entangle server --bind 0.0.0.0:8080 --with-api
+```
+
+## Development
+
+Requires [Hermit](https://cashapp.github.io/hermit/) for toolchain management.
+
+```sh
+just init # set up environment
+just dev # run the app
+just ci # clippy + format check + tests
+```
\ No newline at end of file
diff --git a/bin/.gitignore b/bin/.gitignore
new file mode 100644
index 0000000..79b6ec0
--- /dev/null
+++ b/bin/.gitignore
@@ -0,0 +1,4 @@
+*
+
+!.gitignore
+!package
\ No newline at end of file
diff --git a/bin/.just-1.46.0.pkg b/bin/.just-1.46.0.pkg
deleted file mode 120000
index 383f451..0000000
--- a/bin/.just-1.46.0.pkg
+++ /dev/null
@@ -1 +0,0 @@
-hermit
\ No newline at end of file
diff --git a/bin/.rustup-1.28.2.pkg b/bin/.rustup-1.28.2.pkg
deleted file mode 120000
index 383f451..0000000
--- a/bin/.rustup-1.28.2.pkg
+++ /dev/null
@@ -1 +0,0 @@
-hermit
\ No newline at end of file
diff --git a/bin/README.hermit.md b/bin/README.hermit.md
deleted file mode 100644
index e889550..0000000
--- a/bin/README.hermit.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Hermit environment
-
-This is a [Hermit](https://github.com/cashapp/hermit) bin directory.
-
-The symlinks in this directory are managed by Hermit and will automatically
-download and install Hermit itself as well as packages. These packages are
-local to this environment.
diff --git a/bin/activate-hermit b/bin/activate-hermit
deleted file mode 100755
index fe28214..0000000
--- a/bin/activate-hermit
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-# This file must be used with "source bin/activate-hermit" from bash or zsh.
-# You cannot run it directly
-#
-# THIS FILE IS GENERATED; DO NOT MODIFY
-
-if [ "${BASH_SOURCE-}" = "$0" ]; then
- echo "You must source this script: \$ source $0" >&2
- exit 33
-fi
-
-BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")"
-if "${BIN_DIR}/hermit" noop > /dev/null; then
- eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")"
-
- if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then
- hash -r 2>/dev/null
- fi
-
- echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated"
-fi
diff --git a/bin/activate-hermit.fish b/bin/activate-hermit.fish
deleted file mode 100755
index 0367d23..0000000
--- a/bin/activate-hermit.fish
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env fish
-
-# This file must be sourced with "source bin/activate-hermit.fish" from Fish shell.
-# You cannot run it directly.
-#
-# THIS FILE IS GENERATED; DO NOT MODIFY
-
-if status is-interactive
- set BIN_DIR (dirname (status --current-filename))
-
- if "$BIN_DIR/hermit" noop > /dev/null
- # Source the activation script generated by Hermit
- "$BIN_DIR/hermit" activate "$BIN_DIR/.." | source
-
- # Clear the command cache if applicable
- functions -c > /dev/null 2>&1
-
- # Display activation message
- echo "Hermit environment $($HERMIT_ENV/bin/hermit env HERMIT_ENV) activated"
- end
-else
- echo "You must source this script: source $argv[0]" >&2
- exit 33
-end
diff --git a/bin/cargo b/bin/cargo
deleted file mode 120000
index 906ee94..0000000
--- a/bin/cargo
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/cargo-clippy b/bin/cargo-clippy
deleted file mode 120000
index 906ee94..0000000
--- a/bin/cargo-clippy
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/cargo-fmt b/bin/cargo-fmt
deleted file mode 120000
index 906ee94..0000000
--- a/bin/cargo-fmt
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/cargo-miri b/bin/cargo-miri
deleted file mode 120000
index 906ee94..0000000
--- a/bin/cargo-miri
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/clippy-driver b/bin/clippy-driver
deleted file mode 120000
index 906ee94..0000000
--- a/bin/clippy-driver
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/hermit b/bin/hermit
deleted file mode 100755
index 31559b7..0000000
--- a/bin/hermit
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/bin/bash
-#
-# THIS FILE IS GENERATED; DO NOT MODIFY
-
-set -eo pipefail
-
-export HERMIT_USER_HOME=~
-
-if [ -z "${HERMIT_STATE_DIR}" ]; then
- case "$(uname -s)" in
- Darwin)
- export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit"
- ;;
- Linux)
- export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit"
- ;;
- esac
-fi
-
-export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}"
-HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")"
-export HERMIT_CHANNEL
-export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit}
-
-if [ ! -x "${HERMIT_EXE}" ]; then
- echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2
- INSTALL_SCRIPT="$(mktemp)"
- # This value must match that of the install script
- INSTALL_SCRIPT_SHA256="09ed936378857886fd4a7a4878c0f0c7e3d839883f39ca8b4f2f242e3126e1c6"
- if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then
- curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}"
- else
- # Install script is versioned by its sha256sum value
- curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}"
- # Verify install script's sha256sum
- openssl dgst -sha256 "${INSTALL_SCRIPT}" | \
- awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \
- '$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}'
- fi
- /bin/bash "${INSTALL_SCRIPT}" 1>&2
-fi
-
-exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@"
diff --git a/bin/hermit.hcl b/bin/hermit.hcl
deleted file mode 100644
index 081cbe8..0000000
--- a/bin/hermit.hcl
+++ /dev/null
@@ -1,2 +0,0 @@
-github-token-auth {
-}
diff --git a/bin/just b/bin/just
deleted file mode 120000
index 816066f..0000000
--- a/bin/just
+++ /dev/null
@@ -1 +0,0 @@
-.just-1.46.0.pkg
\ No newline at end of file
diff --git a/bin/rls b/bin/rls
deleted file mode 120000
index 906ee94..0000000
--- a/bin/rls
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/rust-analyzer b/bin/rust-analyzer
deleted file mode 120000
index 906ee94..0000000
--- a/bin/rust-analyzer
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/rust-gdb b/bin/rust-gdb
deleted file mode 120000
index 906ee94..0000000
--- a/bin/rust-gdb
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/rust-gdbgui b/bin/rust-gdbgui
deleted file mode 120000
index 906ee94..0000000
--- a/bin/rust-gdbgui
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/rust-lldb b/bin/rust-lldb
deleted file mode 120000
index 906ee94..0000000
--- a/bin/rust-lldb
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/rustc b/bin/rustc
deleted file mode 120000
index 906ee94..0000000
--- a/bin/rustc
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/rustdoc b/bin/rustdoc
deleted file mode 120000
index 906ee94..0000000
--- a/bin/rustdoc
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/rustfmt b/bin/rustfmt
deleted file mode 120000
index 906ee94..0000000
--- a/bin/rustfmt
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/bin/rustup b/bin/rustup
deleted file mode 120000
index 906ee94..0000000
--- a/bin/rustup
+++ /dev/null
@@ -1 +0,0 @@
-.rustup-1.28.2.pkg
\ No newline at end of file
diff --git a/justfile b/justfile
index 4dd4dc2..230d1fd 100644
--- a/justfile
+++ b/justfile
@@ -1,6 +1,8 @@
set dotenv-load
set dotenv-filename := ".env.local"
+import? 'justfile.local'
+
dev:
cargo run
diff --git a/src/api.rs b/src/api.rs
new file mode 100644
index 0000000..bc15144
--- /dev/null
+++ b/src/api.rs
@@ -0,0 +1,48 @@
+use axum::{Json, Router, routing::post};
+
+use crate::swap::{SwapError, SwapRequest, build_swap_psbt};
+
+/// Build the API router with all routes.
+pub fn router() -> Router {
+ Router::new().route("/swap", post(create_swap))
+}
+
+/// POST /swap — Build a swap PSBT from a SwapRequest.
+async fn create_swap(Json(request): Json) -> Result, ApiError> {
+ let psbt = build_swap_psbt(&request).map_err(ApiError::Swap)?;
+ Ok(Json(SwapResponse {
+ psbt_base64: psbt.to_string(),
+ }))
+}
+
+/// Response body for the swap endpoint.
+#[derive(serde::Serialize)]
+pub struct SwapResponse {
+ /// The unsigned PSBT encoded as a base64 string.
+ pub psbt_base64: String,
+}
+
+/// API error wrapper.
+pub enum ApiError {
+ Swap(SwapError),
+}
+
+impl axum::response::IntoResponse for ApiError {
+ fn into_response(self) -> axum::response::Response {
+ let (status, message) = match self {
+ ApiError::Swap(e) => (axum::http::StatusCode::BAD_REQUEST, e.to_string()),
+ };
+ let body = serde_json::json!({ "error": message });
+ (status, Json(body)).into_response()
+ }
+}
+
+/// Start the API server, binding to the given address.
+pub async fn serve(bind: &str) {
+ let app = router();
+ let listener = tokio::net::TcpListener::bind(bind)
+ .await
+ .expect("failed to bind API server");
+ println!("API server listening on {bind}");
+ axum::serve(listener, app).await.expect("API server error");
+}
diff --git a/src/bin/main.rs b/src/bin/main.rs
index d3eb841..d94a7be 100644
--- a/src/bin/main.rs
+++ b/src/bin/main.rs
@@ -1,3 +1,4 @@
-fn main() {
- entangle::main();
+#[tokio::main]
+async fn main() {
+ entangle::main().await;
}
diff --git a/src/lib.rs b/src/lib.rs
index 03746aa..69d8ca3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,9 +1,44 @@
-use clap::Parser;
+pub mod api;
+pub mod server;
+pub mod swap;
+
+use clap::{Parser, Subcommand};
#[derive(Parser)]
-#[command(version, author)]
-struct Args {}
+#[command(version, author, about = "Swaps made easy")]
+pub struct Args {
+ #[command(subcommand)]
+ pub command: Command,
+}
+
+#[derive(Subcommand)]
+pub enum Command {
+ /// Start the REST API server.
+ Api {
+ /// Address to bind the API server to.
+ #[arg(long, default_value = "0.0.0.0:3000", env = "ENTANGLE_API_BIND")]
+ bind: String,
+ },
+ /// Start the frontend web server.
+ Server {
+ /// Address to bind the frontend server to.
+ #[arg(long, default_value = "0.0.0.0:8080", env = "ENTANGLE_SERVER_BIND")]
+ bind: String,
+ /// Also serve the API routes under /api.
+ #[arg(long)]
+ with_api: bool,
+ },
+}
+
+pub async fn main() {
+ let args = Args::parse();
-pub fn main() {
- Args::parse();
+ match args.command {
+ Command::Api { bind } => {
+ api::serve(&bind).await;
+ }
+ Command::Server { bind, with_api } => {
+ server::serve(&bind, with_api).await;
+ }
+ }
}
diff --git a/src/server.rs b/src/server.rs
new file mode 100644
index 0000000..5ec196d
--- /dev/null
+++ b/src/server.rs
@@ -0,0 +1,34 @@
+use axum::{Router, routing::get};
+
+/// Build the frontend router.
+///
+/// Currently a placeholder. Will be replaced with Leptos SSR routes.
+pub fn router() -> Router {
+ Router::new().route("/", get(index))
+}
+
+async fn index() -> &'static str {
+ "entangle frontend - coming soon"
+}
+
+/// Start the frontend server.
+///
+/// If `with_api` is true, the API routes are nested under `/api`.
+pub async fn serve(bind: &str, with_api: bool) {
+ let mut app = router();
+
+ if with_api {
+ app = app.nest("/api", crate::api::router());
+ }
+
+ let listener = tokio::net::TcpListener::bind(bind)
+ .await
+ .expect("failed to bind frontend server");
+ println!("Frontend server listening on {bind}");
+ if with_api {
+ println!("API routes available at /api/*");
+ }
+ axum::serve(listener, app)
+ .await
+ .expect("frontend server error");
+}
diff --git a/src/swap.rs b/src/swap.rs
new file mode 100644
index 0000000..95c871f
--- /dev/null
+++ b/src/swap.rs
@@ -0,0 +1,130 @@
+use bitcoin::{
+ Address, Network, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness,
+ absolute, transaction,
+};
+use serde::{Deserialize, Serialize};
+use std::fmt;
+
+/// A UTXO identified by its outpoint and the output it represents.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Utxo {
+ /// The outpoint (txid:vout) identifying this UTXO.
+ pub outpoint: OutPoint,
+ /// The transaction output (value + scriptPubKey).
+ pub txout: TxOut,
+}
+
+/// A request to build a swap PSBT.
+///
+/// Party A's UTXO is always required. Party B's UTXO is optional —
+/// if absent, the PSBT spends only A's UTXO to B's address.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SwapRequest {
+ /// The UTXO owned by party A.
+ pub utxo_a: Utxo,
+ /// The address where party A wants to receive funds.
+ pub address_a: Address,
+ /// The UTXO owned by party B (optional).
+ pub utxo_b: Option,
+ /// The address where party B wants to receive funds.
+ pub address_b: Address,
+ /// The bitcoin network to validate addresses against.
+ pub network: Network,
+}
+
+/// Errors that can occur when building a swap PSBT.
+#[derive(Debug)]
+pub enum SwapError {
+ /// An address does not belong to the expected network.
+ NetworkMismatch { expected: Network, address: String },
+ /// The unsigned transaction could not be converted to a PSBT.
+ PsbtCreation(String),
+}
+
+impl fmt::Display for SwapError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ SwapError::NetworkMismatch { expected, address } => {
+ write!(f, "address {address} does not match network {expected}")
+ }
+ SwapError::PsbtCreation(msg) => write!(f, "failed to create PSBT: {msg}"),
+ }
+ }
+}
+
+impl std::error::Error for SwapError {}
+
+/// Validate an unchecked address against the expected network.
+fn validate_address(
+ address: &Address,
+ network: Network,
+) -> Result {
+ let addr_string = address.clone().assume_checked().to_string();
+ address
+ .clone()
+ .require_network(network)
+ .map_err(|_| SwapError::NetworkMismatch {
+ expected: network,
+ address: addr_string,
+ })
+}
+
+/// Build a PSBT that swaps UTXOs between two parties.
+///
+/// The resulting PSBT contains:
+/// - An input spending `utxo_a` with an output paying to `address_b`
+/// - If `utxo_b` is provided: an input spending `utxo_b` with an output paying to `address_a`
+///
+/// The PSBT is unsigned. Party A signs first, then party B.
+pub fn build_swap_psbt(request: &SwapRequest) -> Result {
+ let address_a = validate_address(&request.address_a, request.network)?;
+ let address_b = validate_address(&request.address_b, request.network)?;
+
+ // Build inputs
+ let mut inputs = vec![TxIn {
+ previous_output: request.utxo_a.outpoint,
+ script_sig: ScriptBuf::new(),
+ sequence: Sequence::MAX,
+ witness: Witness::default(),
+ }];
+
+ if let Some(ref utxo_b) = request.utxo_b {
+ inputs.push(TxIn {
+ previous_output: utxo_b.outpoint,
+ script_sig: ScriptBuf::new(),
+ sequence: Sequence::MAX,
+ witness: Witness::default(),
+ });
+ }
+
+ // Build outputs: A's value → B's address, B's value → A's address
+ let mut outputs = vec![TxOut {
+ value: request.utxo_a.txout.value,
+ script_pubkey: address_b.script_pubkey(),
+ }];
+
+ if let Some(ref utxo_b) = request.utxo_b {
+ outputs.push(TxOut {
+ value: utxo_b.txout.value,
+ script_pubkey: address_a.script_pubkey(),
+ });
+ }
+
+ let tx = Transaction {
+ version: transaction::Version::TWO,
+ lock_time: absolute::LockTime::ZERO,
+ input: inputs,
+ output: outputs,
+ };
+
+ let mut psbt =
+ Psbt::from_unsigned_tx(tx).map_err(|e| SwapError::PsbtCreation(e.to_string()))?;
+
+ // Set witness_utxo on PSBT inputs for signing support
+ psbt.inputs[0].witness_utxo = Some(request.utxo_a.txout.clone());
+ if let Some(ref utxo_b) = request.utxo_b {
+ psbt.inputs[1].witness_utxo = Some(utxo_b.txout.clone());
+ }
+
+ Ok(psbt)
+}
diff --git a/tests/api.rs b/tests/api.rs
new file mode 100644
index 0000000..ccf1f1c
--- /dev/null
+++ b/tests/api.rs
@@ -0,0 +1,70 @@
+use axum::body::Body;
+use axum::http::{Request, StatusCode};
+use http_body_util::BodyExt;
+use tower::ServiceExt;
+
+use crate::*;
+
+#[tokio::test]
+async fn post_swap_returns_200() {
+ let app = entangle::api::router();
+ let response = app
+ .oneshot(
+ Request::builder()
+ .method("POST")
+ .uri("/swap")
+ .header("content-type", "application/json")
+ .body(Body::from(swap_request_json(true)))
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::OK);
+
+ let body = response.into_body().collect().await.unwrap().to_bytes();
+ let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
+ let psbt_str = json["psbt_base64"].as_str().unwrap();
+ // Verify it parses as a valid PSBT
+ let _: bitcoin::Psbt = psbt_str.parse().unwrap();
+}
+
+#[tokio::test]
+async fn post_swap_network_mismatch_returns_400() {
+ let app = entangle::api::router();
+ let response = app
+ .oneshot(
+ Request::builder()
+ .method("POST")
+ .uri("/swap")
+ .header("content-type", "application/json")
+ .body(Body::from(mismatch_request_json()))
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+
+ let body = response.into_body().collect().await.unwrap().to_bytes();
+ let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
+ assert!(json.get("error").is_some());
+}
+
+#[tokio::test]
+async fn post_swap_invalid_json_returns_400() {
+ let app = entangle::api::router();
+ let response = app
+ .oneshot(
+ Request::builder()
+ .method("POST")
+ .uri("/swap")
+ .header("content-type", "application/json")
+ .body(Body::from("not json"))
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+}
diff --git a/tests/lib.rs b/tests/lib.rs
index 8b13789..4da0f41 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -1 +1,56 @@
+mod api;
+mod server;
+mod swap;
+use std::str::FromStr;
+
+use bitcoin::{Amount, Network, OutPoint, ScriptBuf, TxOut};
+
+use entangle::swap::{SwapRequest, Utxo};
+
+pub const TESTNET_ADDR_A: &str = "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7";
+pub const TESTNET_ADDR_B: &str = "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx";
+pub const MAINNET_ADDR: &str = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
+
+pub fn make_utxo(sats: u64, vout: u32) -> Utxo {
+ let outpoint = OutPoint::from_str(&format!(
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:{vout}"
+ ))
+ .unwrap();
+ Utxo {
+ outpoint,
+ txout: TxOut {
+ value: Amount::from_sat(sats),
+ script_pubkey: ScriptBuf::new(),
+ },
+ }
+}
+
+pub fn make_swap_request(two_sided: bool) -> SwapRequest {
+ SwapRequest {
+ utxo_a: make_utxo(50_000, 0),
+ address_a: TESTNET_ADDR_A.parse().unwrap(),
+ utxo_b: if two_sided {
+ Some(make_utxo(75_000, 1))
+ } else {
+ None
+ },
+ address_b: TESTNET_ADDR_B.parse().unwrap(),
+ network: Network::Testnet,
+ }
+}
+
+pub fn swap_request_json(two_sided: bool) -> String {
+ serde_json::to_string(&make_swap_request(two_sided)).unwrap()
+}
+
+pub fn mismatch_request_json() -> String {
+ let request = SwapRequest {
+ utxo_a: make_utxo(50_000, 0),
+ address_a: MAINNET_ADDR.parse().unwrap(),
+ utxo_b: None,
+ address_b: TESTNET_ADDR_B.parse().unwrap(),
+ network: Network::Testnet,
+ };
+ serde_json::to_string(&request).unwrap()
+}
diff --git a/tests/server.rs b/tests/server.rs
new file mode 100644
index 0000000..e89a4bb
--- /dev/null
+++ b/tests/server.rs
@@ -0,0 +1,38 @@
+use axum::body::Body;
+use axum::http::{Request, StatusCode};
+use http_body_util::BodyExt;
+use tower::ServiceExt;
+
+use crate::*;
+
+#[tokio::test]
+async fn get_index_returns_placeholder() {
+ let app = entangle::server::router();
+ let response = app
+ .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::OK);
+
+ let body = response.into_body().collect().await.unwrap().to_bytes();
+ assert_eq!(&body[..], b"entangle frontend - coming soon");
+}
+
+#[tokio::test]
+async fn with_api_nesting() {
+ let app = entangle::server::router().nest("/api", entangle::api::router());
+ let response = app
+ .oneshot(
+ Request::builder()
+ .method("POST")
+ .uri("/api/swap")
+ .header("content-type", "application/json")
+ .body(Body::from(swap_request_json(true)))
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::OK);
+}
diff --git a/tests/swap.rs b/tests/swap.rs
new file mode 100644
index 0000000..990ee49
--- /dev/null
+++ b/tests/swap.rs
@@ -0,0 +1,121 @@
+use bitcoin::Network;
+
+use entangle::swap::{SwapRequest, build_swap_psbt};
+
+use crate::*;
+
+#[test]
+fn two_sided_swap_produces_valid_psbt() {
+ let request = make_swap_request(true);
+ let psbt = build_swap_psbt(&request).unwrap();
+
+ assert_eq!(psbt.unsigned_tx.input.len(), 2);
+ assert_eq!(psbt.unsigned_tx.output.len(), 2);
+
+ // Inputs reference the correct outpoints
+ assert_eq!(
+ psbt.unsigned_tx.input[0].previous_output,
+ request.utxo_a.outpoint
+ );
+ assert_eq!(
+ psbt.unsigned_tx.input[1].previous_output,
+ request.utxo_b.as_ref().unwrap().outpoint
+ );
+
+ // Output 0: A's value → B's address
+ assert_eq!(psbt.unsigned_tx.output[0].value, request.utxo_a.txout.value);
+ let addr_b = request
+ .address_b
+ .clone()
+ .require_network(request.network)
+ .unwrap();
+ assert_eq!(
+ psbt.unsigned_tx.output[0].script_pubkey,
+ addr_b.script_pubkey()
+ );
+
+ // Output 1: B's value → A's address
+ assert_eq!(
+ psbt.unsigned_tx.output[1].value,
+ request.utxo_b.as_ref().unwrap().txout.value
+ );
+ let addr_a = request
+ .address_a
+ .clone()
+ .require_network(request.network)
+ .unwrap();
+ assert_eq!(
+ psbt.unsigned_tx.output[1].script_pubkey,
+ addr_a.script_pubkey()
+ );
+
+ // Witness UTXOs set for signing
+ assert_eq!(
+ psbt.inputs[0].witness_utxo,
+ Some(request.utxo_a.txout.clone())
+ );
+ assert_eq!(
+ psbt.inputs[1].witness_utxo,
+ Some(request.utxo_b.as_ref().unwrap().txout.clone())
+ );
+}
+
+#[test]
+fn one_sided_swap_produces_valid_psbt() {
+ let request = make_swap_request(false);
+ let psbt = build_swap_psbt(&request).unwrap();
+
+ assert_eq!(psbt.unsigned_tx.input.len(), 1);
+ assert_eq!(psbt.unsigned_tx.output.len(), 1);
+ assert_eq!(
+ psbt.unsigned_tx.input[0].previous_output,
+ request.utxo_a.outpoint
+ );
+ assert_eq!(psbt.unsigned_tx.output[0].value, request.utxo_a.txout.value);
+ assert_eq!(
+ psbt.inputs[0].witness_utxo,
+ Some(request.utxo_a.txout.clone())
+ );
+}
+
+#[test]
+fn network_mismatch_address_a() {
+ let request = SwapRequest {
+ utxo_a: make_utxo(50_000, 0),
+ address_a: MAINNET_ADDR.parse().unwrap(),
+ utxo_b: None,
+ address_b: TESTNET_ADDR_B.parse().unwrap(),
+ network: Network::Testnet,
+ };
+ let err = build_swap_psbt(&request).unwrap_err();
+ assert!(matches!(
+ err,
+ entangle::swap::SwapError::NetworkMismatch { .. }
+ ));
+}
+
+#[test]
+fn network_mismatch_address_b() {
+ let request = SwapRequest {
+ utxo_a: make_utxo(50_000, 0),
+ address_a: TESTNET_ADDR_A.parse().unwrap(),
+ utxo_b: None,
+ address_b: MAINNET_ADDR.parse().unwrap(),
+ network: Network::Testnet,
+ };
+ let err = build_swap_psbt(&request).unwrap_err();
+ assert!(matches!(
+ err,
+ entangle::swap::SwapError::NetworkMismatch { .. }
+ ));
+}
+
+#[test]
+fn psbt_base64_round_trips() {
+ let request = make_swap_request(true);
+ let psbt = build_swap_psbt(&request).unwrap();
+ let base64_str = psbt.to_string();
+ let parsed: bitcoin::Psbt = base64_str.parse().unwrap();
+ assert_eq!(parsed.unsigned_tx.input.len(), 2);
+ assert_eq!(parsed.unsigned_tx.output.len(), 2);
+}
From 9074215dc73cf279a43b7dcf80eed807a01e3ffc Mon Sep 17 00:00:00 2001
From: parabit
Date: Sun, 15 Feb 2026 20:32:53 -0600
Subject: [PATCH 2/7] leptos for frontend
---
Cargo.lock | 2780 ++++++++++++++++++++++++++++++++++++++++-----
Cargo.toml | 71 +-
justfile | 4 +-
src/app.rs | 50 +
src/lib.rs | 16 +
src/server.rs | 34 -
src/server/mod.rs | 46 +
tests/server.rs | 20 +-
8 files changed, 2695 insertions(+), 326 deletions(-)
create mode 100644 src/app.rs
delete mode 100644 src/server.rs
create mode 100644 src/server/mod.rs
diff --git a/Cargo.lock b/Cargo.lock
index dffceae..3de8947 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "anstream"
version = "0.6.21"
@@ -52,18 +61,94 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "any_spawner"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d"
+dependencies = [
+ "futures",
+ "thiserror 2.0.18",
+ "tokio",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
+
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-once-cell"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+[[package]]
+name = "attribute-derive"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77"
+dependencies = [
+ "attribute-derive-macro",
+ "derive-where",
+ "manyhow",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "attribute-derive-macro"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61"
+dependencies = [
+ "collection_literals",
+ "interpolator",
+ "manyhow",
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "quote-use",
+ "syn",
+]
+
[[package]]
name = "axum"
version = "0.8.8"
@@ -71,6 +156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
+ "base64 0.22.1",
"bytes",
"form_urlencoded",
"futures-util",
@@ -83,14 +169,17 @@ dependencies = [
"matchit",
"memchr",
"mime",
+ "multer",
"percent-encoding",
"pin-project-lite",
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
+ "sha1",
"sync_wrapper",
"tokio",
+ "tokio-tungstenite",
"tower",
"tower-layer",
"tower-service",
@@ -116,6 +205,12 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "base16"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8"
+
[[package]]
name = "base58ck"
version = "0.1.0"
@@ -132,6 +227,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
[[package]]
name = "bech32"
version = "0.11.1"
@@ -145,7 +246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66"
dependencies = [
"base58ck",
- "base64",
+ "base64 0.21.7",
"bech32",
"bitcoin-internals",
"bitcoin-io",
@@ -193,12 +294,39 @@ dependencies = [
"serde",
]
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[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 = "bumpalo"
+version = "3.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
+
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+[[package]]
+name = "camino"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+
[[package]]
name = "cc"
version = "1.2.56"
@@ -209,6 +337,12 @@ dependencies = [
"shlex",
]
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
[[package]]
name = "clap"
version = "4.5.57"
@@ -250,326 +384,1897 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
-name = "colorchoice"
-version = "1.0.4"
+name = "codee"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
-
-[[package]]
-name = "entangle"
-version = "0.0.1"
+checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5"
dependencies = [
- "axum",
- "bitcoin",
- "clap",
- "http-body-util",
"serde",
"serde_json",
- "tokio",
- "tower",
+ "thiserror 2.0.18",
]
[[package]]
-name = "find-msvc-tools"
-version = "0.1.9"
+name = "collection_literals"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
[[package]]
-name = "form_urlencoded"
-version = "1.2.2"
+name = "colorchoice"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
- "percent-encoding",
+ "crossbeam-utils",
]
[[package]]
-name = "futures-channel"
-version = "0.3.32"
+name = "config"
+version = "0.15.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6"
dependencies = [
- "futures-core",
+ "convert_case 0.6.0",
+ "pathdiff",
+ "serde_core",
+ "toml",
+ "winnow",
]
[[package]]
-name = "futures-core"
-version = "0.3.32"
+name = "console_error_panic_hook"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
[[package]]
-name = "futures-task"
-version = "0.3.32"
+name = "const-str"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+checksum = "b0664d2867b4a32697dfe655557f5c3b187e9b605b38612a748e5ec99811d160"
[[package]]
-name = "futures-util"
-version = "0.3.32"
+name = "const_format"
+version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad"
dependencies = [
- "futures-core",
- "futures-task",
- "pin-project-lite",
- "slab",
+ "const_format_proc_macros",
]
[[package]]
-name = "heck"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-
-[[package]]
-name = "hex-conservative"
-version = "0.2.2"
+name = "const_format_proc_macros"
+version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
dependencies = [
- "arrayvec",
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
]
[[package]]
-name = "hex_lit"
-version = "0.1.1"
+name = "const_str_slice_concat"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
+checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b"
[[package]]
-name = "http"
-version = "1.4.0"
+name = "convert_case"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
- "bytes",
- "itoa",
+ "unicode-segmentation",
]
[[package]]
-name = "http-body"
-version = "1.0.1"
+name = "convert_case"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
- "bytes",
- "http",
+ "unicode-segmentation",
]
[[package]]
-name = "http-body-util"
-version = "0.1.3"
+name = "convert_case"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
- "bytes",
- "futures-core",
- "http",
- "http-body",
- "pin-project-lite",
+ "unicode-segmentation",
]
[[package]]
-name = "httparse"
-version = "1.10.1"
+name = "cpufeatures"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
[[package]]
-name = "httpdate"
-version = "1.0.3"
+name = "crossbeam-utils"
+version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
-name = "hyper"
-version = "1.8.1"
+name = "crypto-common"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
- "atomic-waker",
- "bytes",
- "futures-channel",
- "futures-core",
- "http",
- "http-body",
- "httparse",
- "httpdate",
- "itoa",
- "pin-project-lite",
- "pin-utils",
- "smallvec",
- "tokio",
+ "generic-array",
+ "typenum",
]
[[package]]
-name = "hyper-util"
-version = "0.1.20"
+name = "dashmap"
+version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
- "bytes",
- "http",
- "http-body",
- "hyper",
- "pin-project-lite",
- "tokio",
- "tower-service",
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
]
[[package]]
-name = "is_terminal_polyfill"
-version = "1.70.2"
+name = "data-encoding"
+version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
-name = "itoa"
-version = "1.0.17"
+name = "derive-where"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "libc"
-version = "0.2.182"
+name = "digest"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
[[package]]
-name = "log"
-version = "0.4.29"
+name = "displaydoc"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "matchit"
-version = "0.8.4"
+name = "drain_filter_polyfill"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
[[package]]
-name = "memchr"
-version = "2.8.0"
+name = "either"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
-name = "mime"
-version = "0.3.17"
+name = "either_of"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+checksum = "216d23e0ec69759a17f05e1c553f3a6870e5ec73420fbb07807a6f34d5d1d5a4"
+dependencies = [
+ "paste",
+ "pin-project-lite",
+]
[[package]]
-name = "mio"
-version = "1.1.1"
+name = "encoding_rs"
+version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
- "libc",
- "wasi",
- "windows-sys 0.61.2",
+ "cfg-if",
]
[[package]]
-name = "once_cell"
-version = "1.21.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+name = "entangle"
+version = "0.0.1"
+dependencies = [
+ "axum",
+ "bitcoin",
+ "clap",
+ "console_error_panic_hook",
+ "http-body-util",
+ "leptos",
+ "leptos_axum",
+ "leptos_meta",
+ "leptos_router",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tower",
+ "wasm-bindgen",
+]
[[package]]
-name = "once_cell_polyfill"
-version = "1.70.2"
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472"
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[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.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "gloo-net"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils",
+ "http",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "guardian"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex-conservative"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "hex_lit"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
+
+[[package]]
+name = "html-escape"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
+dependencies = [
+ "utf8-width",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-range-header"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hydration_context"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8714ae4adeaa846d838f380fbd72f049197de629948f91bf045329e0cf0a283"
+dependencies = [
+ "futures",
+ "js-sys",
+ "once_cell",
+ "or_poisoned",
+ "pin-project-lite",
+ "serde",
+ "throw_error",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "hyper"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "bytes",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+[[package]]
+name = "icu_properties"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
+
+[[package]]
+name = "icu_provider"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "interpolator"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
+
+[[package]]
+name = "inventory"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "js-sys"
+version = "0.3.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "leptos"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f9569fc37575a5d64c0512145af7630bf651007237ef67a8a77328199d315bb"
+dependencies = [
+ "any_spawner",
+ "base64 0.22.1",
+ "cfg-if",
+ "either_of",
+ "futures",
+ "getrandom 0.3.4",
+ "hydration_context",
+ "leptos_config",
+ "leptos_dom",
+ "leptos_hot_reload",
+ "leptos_macro",
+ "leptos_server",
+ "oco_ref",
+ "or_poisoned",
+ "paste",
+ "rand",
+ "reactive_graph",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "server_fn",
+ "slotmap",
+ "tachys",
+ "thiserror 2.0.18",
+ "throw_error",
+ "typed-builder 0.23.2",
+ "typed-builder-macro 0.23.2",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm_split_helpers",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_axum"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0caa95760f87f3067e05025140becefdbdfd36cbc2adac4519f06e1f1edf4af"
+dependencies = [
+ "any_spawner",
+ "axum",
+ "dashmap",
+ "futures",
+ "hydration_context",
+ "leptos",
+ "leptos_integration_utils",
+ "leptos_macro",
+ "leptos_meta",
+ "leptos_router",
+ "parking_lot",
+ "server_fn",
+ "tachys",
+ "tokio",
+ "tower",
+ "tower-http",
+]
+
+[[package]]
+name = "leptos_config"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071fc40aeb9fcab885965bad1887990477253ad51f926cd19068f45a44c59e89"
+dependencies = [
+ "config",
+ "regex",
+ "serde",
+ "thiserror 2.0.18",
+ "typed-builder 0.21.2",
+]
+
+[[package]]
+name = "leptos_dom"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78f4330c88694c5575e0bfe4eecf81b045d14e76a4f8b00d5fd2a63f8779f895"
+dependencies = [
+ "js-sys",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "tachys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_hot_reload"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d61ec3e1ff8aaee8c5151688550c0363f85bc37845450764c31ff7584a33f38"
+dependencies = [
+ "anyhow",
+ "camino",
+ "indexmap",
+ "parking_lot",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "serde",
+ "syn",
+ "walkdir",
+]
+
+[[package]]
+name = "leptos_integration_utils"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13cccc9305df53757bae61bf15641bfa6a667b5f78456ace4879dfe0591ae0e8"
+dependencies = [
+ "futures",
+ "hydration_context",
+ "leptos",
+ "leptos_config",
+ "leptos_meta",
+ "leptos_router",
+ "reactive_graph",
+]
+
+[[package]]
+name = "leptos_macro"
+version = "0.8.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86ffd2e9cf3e264e9b3e16bdb086cefa26bd0fa7bc6a26b0cc5f6c1fd3178ed"
+dependencies = [
+ "attribute-derive",
+ "cfg-if",
+ "convert_case 0.10.0",
+ "html-escape",
+ "itertools",
+ "leptos_hot_reload",
+ "prettyplease",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "rustc_version",
+ "server_fn_macro",
+ "syn",
+ "uuid",
+]
+
+[[package]]
+name = "leptos_meta"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d489e38d3f541e9e43ecc2e3a815527840345a2afca629b3e23fcc1dd254578"
+dependencies = [
+ "futures",
+ "indexmap",
+ "leptos",
+ "or_poisoned",
+ "send_wrapper",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_router"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01e573711f2fb9ab5d655ec38115220d359eaaf1dcb93cc0ea624543b6dba959"
+dependencies = [
+ "any_spawner",
+ "either_of",
+ "futures",
+ "gloo-net",
+ "js-sys",
+ "leptos",
+ "leptos_router_macro",
+ "or_poisoned",
+ "percent-encoding",
+ "reactive_graph",
+ "rustc_version",
+ "send_wrapper",
+ "tachys",
+ "thiserror 2.0.18",
+ "url",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_router_macro"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "409c0bd99f986c3cfa1a4db2443c835bc602ded1a12784e22ecb28c3ed5a2ae2"
+dependencies = [
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "leptos_server"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf1045af93050bf3388d1c138426393fc131f6d9e46a65519da884c033ed730"
+dependencies = [
+ "any_spawner",
+ "base64 0.22.1",
+ "codee",
+ "futures",
+ "hydration_context",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "server_fn",
+ "tachys",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.182"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
+
+[[package]]
+name = "linear-map"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee"
+
+[[package]]
+name = "litemap"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "manyhow"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
+dependencies = [
+ "manyhow-macros",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "manyhow-macros"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[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.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "mio"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "multer"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
+dependencies = [
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http",
+ "httparse",
+ "memchr",
+ "mime",
+ "spin",
+ "version_check",
+]
+
+[[package]]
+name = "next_tuple"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
+
+[[package]]
+name = "oco_ref"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d"
+dependencies = [
+ "serde",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
-name = "percent-encoding"
-version = "2.3.2"
+name = "or_poisoned"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-utils"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "smallvec",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "quote-use"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e"
+dependencies = [
+ "quote",
+ "quote-use-macros",
+]
+
+[[package]]
+name = "quote-use-macros"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "reactive_graph"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17f0df355582937223ea403e52490201d65295bd6981383c69bfae5a1f8730c2"
+dependencies = [
+ "any_spawner",
+ "async-lock",
+ "futures",
+ "guardian",
+ "hydration_context",
+ "indexmap",
+ "or_poisoned",
+ "paste",
+ "pin-project-lite",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "serde",
+ "slotmap",
+ "thiserror 2.0.18",
+ "web-sys",
+]
+
+[[package]]
+name = "reactive_stores"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35372f05664a62a3dd389503371a15b8feb3396f99f6ec000de651fddb030942"
+dependencies = [
+ "dashmap",
+ "guardian",
+ "itertools",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "reactive_stores_macro",
+ "rustc-hash",
+ "send_wrapper",
+]
+
+[[package]]
+name = "reactive_stores_macro"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fa40919eb2975100283b2a70e68eafce1e8bcf81f0622ff168e4c2b3f8d46bb"
+dependencies = [
+ "convert_case 0.8.0",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
+
+[[package]]
+name = "rstml"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56"
+dependencies = [
+ "derive-where",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn",
+ "syn_derive",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "secp256k1"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
+dependencies = [
+ "bitcoin_hashes",
+ "secp256k1-sys",
+ "serde",
+]
+
+[[package]]
+name = "secp256k1-sys"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "send_wrapper"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_qs"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352"
+dependencies = [
+ "percent-encoding",
+ "serde",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
+dependencies = [
+ "serde_core",
+]
+
+[[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 = "server_fn"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353d02fa2886cd8dae0b8da0965289fa8f2ecc7df633d1ce965f62fdf9644d29"
+dependencies = [
+ "axum",
+ "base64 0.22.1",
+ "bytes",
+ "const-str",
+ "const_format",
+ "dashmap",
+ "futures",
+ "gloo-net",
+ "http",
+ "http-body-util",
+ "hyper",
+ "inventory",
+ "js-sys",
+ "pin-project-lite",
+ "rustc_version",
+ "rustversion",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "server_fn_macro_default",
+ "thiserror 2.0.18",
+ "throw_error",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "950b8cfc9ff5f39ca879c5a7c5e640de2695a199e18e424c3289d0964cabe642"
+dependencies = [
+ "const_format",
+ "convert_case 0.8.0",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro_default"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00"
+dependencies = [
+ "server_fn_macro",
+ "syn",
+]
+
+[[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.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "slotmap"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
-name = "pin-project-lite"
-version = "0.2.16"
+name = "stable_deref_trait"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
-name = "pin-utils"
-version = "0.1.0"
+name = "strsim"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
-name = "proc-macro2"
-version = "1.0.106"
+name = "syn"
+version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
+ "proc-macro2",
+ "quote",
"unicode-ident",
]
[[package]]
-name = "quote"
-version = "1.0.44"
+name = "syn_derive"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
+checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219"
dependencies = [
+ "proc-macro-error2",
"proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "ryu"
-version = "1.0.23"
+name = "sync_wrapper"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
-name = "secp256k1"
-version = "0.29.1"
+name = "synstructure"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
- "bitcoin_hashes",
- "secp256k1-sys",
- "serde",
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "secp256k1-sys"
-version = "0.10.1"
+name = "tachys"
+version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
+checksum = "f2b2db11e455f7e84e2cc3e76f8a3f3843f7956096265d5ecff781eabe235077"
dependencies = [
- "cc",
+ "any_spawner",
+ "async-trait",
+ "const_str_slice_concat",
+ "drain_filter_polyfill",
+ "either_of",
+ "erased",
+ "futures",
+ "html-escape",
+ "indexmap",
+ "itertools",
+ "js-sys",
+ "linear-map",
+ "next_tuple",
+ "oco_ref",
+ "or_poisoned",
+ "parking_lot",
+ "paste",
+ "reactive_graph",
+ "reactive_stores",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "slotmap",
+ "throw_error",
+ "wasm-bindgen",
+ "web-sys",
]
[[package]]
-name = "serde"
-version = "1.0.228"
+name = "thiserror"
+version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
- "serde_core",
- "serde_derive",
+ "thiserror-impl 1.0.69",
]
[[package]]
-name = "serde_core"
-version = "1.0.228"
+name = "thiserror"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
- "serde_derive",
+ "thiserror-impl 2.0.18",
]
[[package]]
-name = "serde_derive"
-version = "1.0.228"
+name = "thiserror-impl"
+version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
@@ -577,182 +2282,511 @@ dependencies = [
]
[[package]]
-name = "serde_json"
-version = "1.0.149"
+name = "thiserror-impl"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "throw_error"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64"
+dependencies = [
+ "pin-project-lite",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.49.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
- "itoa",
- "memchr",
- "serde",
"serde_core",
- "zmij",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.8+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tungstenite"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand",
+ "sha1",
+ "thiserror 2.0.18",
+ "utf-8",
+]
+
+[[package]]
+name = "typed-builder"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fef81aec2ca29576f9f6ae8755108640d0a86dd3161b2e8bca6cfa554e98f77d"
+dependencies = [
+ "typed-builder-macro 0.21.2",
+]
+
+[[package]]
+name = "typed-builder"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda"
+dependencies = [
+ "typed-builder-macro 0.23.2",
+]
+
+[[package]]
+name = "typed-builder-macro"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ecb9ecf7799210407c14a8cfdfe0173365780968dc57973ed082211958e0b18"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "typed-builder-macro"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "serde_path_to_error"
-version = "0.1.20"
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "url"
+version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
- "itoa",
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
"serde",
- "serde_core",
]
[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
+name = "utf-8"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
- "serde",
+ "getrandom 0.4.1",
+ "js-sys",
+ "wasm-bindgen",
]
[[package]]
-name = "shlex"
-version = "1.3.0"
+name = "version_check"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
-name = "slab"
-version = "0.4.12"
+name = "walkdir"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
[[package]]
-name = "smallvec"
-version = "1.15.1"
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
-name = "socket2"
-version = "0.6.2"
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
- "libc",
- "windows-sys 0.60.2",
+ "wit-bindgen",
]
[[package]]
-name = "strsim"
-version = "0.11.1"
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
[[package]]
-name = "syn"
-version = "2.0.114"
+name = "wasm-bindgen"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
+checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
]
[[package]]
-name = "sync_wrapper"
-version = "1.0.2"
+name = "wasm-bindgen-futures"
+version = "0.4.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
[[package]]
-name = "tokio"
-version = "1.49.0"
+name = "wasm-bindgen-macro"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
+checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
- "libc",
- "mio",
- "pin-project-lite",
- "socket2",
- "tokio-macros",
- "windows-sys 0.61.2",
+ "quote",
+ "wasm-bindgen-macro-support",
]
[[package]]
-name = "tokio-macros"
-version = "2.6.0"
+name = "wasm-bindgen-macro-support"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
+ "bumpalo",
"proc-macro2",
"quote",
"syn",
+ "wasm-bindgen-shared",
]
[[package]]
-name = "tower"
-version = "0.5.3"
+name = "wasm-bindgen-shared"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
- "futures-core",
- "futures-util",
- "pin-project-lite",
- "sync_wrapper",
- "tokio",
- "tower-layer",
- "tower-service",
- "tracing",
+ "unicode-ident",
]
[[package]]
-name = "tower-layer"
-version = "0.3.3"
+name = "wasm-encoder"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
[[package]]
-name = "tower-service"
-version = "0.3.3"
+name = "wasm-metadata"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
[[package]]
-name = "tracing"
-version = "0.1.44"
+name = "wasm-streams"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
- "log",
- "pin-project-lite",
- "tracing-core",
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
]
[[package]]
-name = "tracing-core"
-version = "0.1.36"
+name = "wasm_split_helpers"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+checksum = "a114b3073258dd5de3d812cdd048cca6842342755e828a14dbf15f843f2d1b84"
dependencies = [
- "once_cell",
+ "async-once-cell",
+ "wasm_split_macros",
]
[[package]]
-name = "unicode-ident"
-version = "1.0.22"
+name = "wasm_split_macros"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+checksum = "56481f8ed1a9f9ae97ea7b08a5e2b12e8adf9a7818a6ba952b918e09c7be8bf0"
+dependencies = [
+ "base16",
+ "quote",
+ "sha2",
+ "syn",
+]
[[package]]
-name = "utf8parse"
-version = "0.2.2"
+name = "wasmparser"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
[[package]]
-name = "wasi"
-version = "0.11.1+wasi-snapshot-preview1"
+name = "web-sys"
+version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
[[package]]
name = "windows-link"
@@ -843,6 +2877,218 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+[[package]]
+name = "xxhash-rust"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "yoke"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "zmij"
version = "1.0.21"
diff --git a/Cargo.toml b/Cargo.toml
index 391d840..d35b174 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,7 +12,6 @@ repository.workspace = true
rust-version.workspace = true
[workspace]
-members = ["."]
[workspace.package]
authors = ["The Parasite Devs"]
@@ -25,19 +24,56 @@ rust-version = "1.91.0"
axum = "0.8"
bitcoin = { version = "0.32", features = ["base64", "serde", "std"] }
clap = { version = "4.5.36", features = ["derive", "env"] }
+console_error_panic_hook = "0.1"
http-body-util = "0.1"
+leptos = "0.8"
+leptos_axum = "0.8"
+leptos_meta = "0.8"
+leptos_router = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tower = { version = "0.5", features = ["util"] }
+wasm-bindgen = "0.2"
+
+[features]
+default = ["ssr"]
+ssr = [
+ "dep:axum",
+ "dep:bitcoin",
+ "dep:clap",
+ "dep:leptos_axum",
+ "dep:serde",
+ "dep:serde_json",
+ "dep:tokio",
+ "leptos/ssr",
+ "leptos_meta/ssr",
+ "leptos_router/ssr",
+]
+hydrate = [
+ "dep:console_error_panic_hook",
+ "dep:wasm-bindgen",
+ "leptos/hydrate",
+]
[dependencies]
-axum.workspace = true
-bitcoin.workspace = true
-clap.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-tokio.workspace = true
+# Shared (compiled for both SSR and hydrate)
+leptos = { workspace = true }
+leptos_meta = { workspace = true }
+leptos_router = { workspace = true }
+
+# SSR-only
+axum = { workspace = true, optional = true }
+bitcoin = { workspace = true, optional = true }
+clap = { workspace = true, optional = true }
+leptos_axum = { workspace = true, optional = true }
+serde = { workspace = true, optional = true }
+serde_json = { workspace = true, optional = true }
+tokio = { workspace = true, optional = true }
+
+# Hydrate-only
+console_error_panic_hook = { workspace = true, optional = true }
+wasm-bindgen = { workspace = true, optional = true }
[dev-dependencies]
http-body-util.workspace = true
@@ -46,11 +82,32 @@ tower.workspace = true
[[bin]]
name = "entangle"
path = "src/bin/main.rs"
+required-features = ["ssr"]
[lib]
name = "entangle"
path = "src/lib.rs"
+crate-type = ["cdylib", "rlib"]
[[test]]
name = "integration"
path = "tests/lib.rs"
+required-features = ["ssr"]
+
+[package.metadata.leptos]
+output-name = "entangle"
+site-root = "target/site"
+site-pkg-dir = "pkg"
+site-addr = "127.0.0.1:8080"
+reload-port = 3001
+bin-features = ["ssr"]
+bin-default-features = false
+lib-features = ["hydrate"]
+lib-default-features = false
+lib-profile-release = "wasm-release"
+
+[profile.wasm-release]
+inherits = "release"
+opt-level = "z"
+lto = true
+codegen-units = 1
diff --git a/justfile b/justfile
index 230d1fd..1c13fa8 100644
--- a/justfile
+++ b/justfile
@@ -4,7 +4,7 @@ set dotenv-filename := ".env.local"
import? 'justfile.local'
dev:
- cargo run
+ cargo run -- server
init:
hermit init --quiet
@@ -49,4 +49,4 @@ publish-release revision='master':
git checkout {{ revision }}
cargo publish
cd ../..
- rm -rf tmp/release
\ No newline at end of file
+ rm -rf tmp/release
diff --git a/src/app.rs b/src/app.rs
new file mode 100644
index 0000000..f82ae44
--- /dev/null
+++ b/src/app.rs
@@ -0,0 +1,50 @@
+use leptos::prelude::*;
+use leptos_meta::MetaTags;
+use leptos_router::{
+ StaticSegment,
+ components::{Route, Router, Routes},
+};
+
+/// HTML shell wrapping the App. Used server-side to render the full document.
+#[cfg(feature = "ssr")]
+pub fn shell(options: leptos::config::LeptosOptions) -> impl IntoView {
+ view! {
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+}
+
+/// Root application component, shared between server and client.
+#[component]
+pub fn App() -> impl IntoView {
+ leptos_meta::provide_meta_context();
+
+ view! {
+
+
+ "Page not found"
}>
+
+
+
+
+ }
+}
+
+#[component]
+fn HomePage() -> impl IntoView {
+ view! {
+ "entangle"
+ "Swaps made easy"
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 69d8ca3..ed996ee 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,9 +1,16 @@
+pub mod app;
+
+#[cfg(feature = "ssr")]
pub mod api;
+#[cfg(feature = "ssr")]
pub mod server;
+#[cfg(feature = "ssr")]
pub mod swap;
+#[cfg(feature = "ssr")]
use clap::{Parser, Subcommand};
+#[cfg(feature = "ssr")]
#[derive(Parser)]
#[command(version, author, about = "Swaps made easy")]
pub struct Args {
@@ -11,6 +18,7 @@ pub struct Args {
pub command: Command,
}
+#[cfg(feature = "ssr")]
#[derive(Subcommand)]
pub enum Command {
/// Start the REST API server.
@@ -30,6 +38,7 @@ pub enum Command {
},
}
+#[cfg(feature = "ssr")]
pub async fn main() {
let args = Args::parse();
@@ -42,3 +51,10 @@ pub async fn main() {
}
}
}
+
+#[cfg(feature = "hydrate")]
+#[wasm_bindgen::prelude::wasm_bindgen]
+pub fn hydrate() {
+ console_error_panic_hook::set_once();
+ leptos::mount::hydrate_body(app::App);
+}
diff --git a/src/server.rs b/src/server.rs
deleted file mode 100644
index 5ec196d..0000000
--- a/src/server.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-use axum::{Router, routing::get};
-
-/// Build the frontend router.
-///
-/// Currently a placeholder. Will be replaced with Leptos SSR routes.
-pub fn router() -> Router {
- Router::new().route("/", get(index))
-}
-
-async fn index() -> &'static str {
- "entangle frontend - coming soon"
-}
-
-/// Start the frontend server.
-///
-/// If `with_api` is true, the API routes are nested under `/api`.
-pub async fn serve(bind: &str, with_api: bool) {
- let mut app = router();
-
- if with_api {
- app = app.nest("/api", crate::api::router());
- }
-
- let listener = tokio::net::TcpListener::bind(bind)
- .await
- .expect("failed to bind frontend server");
- println!("Frontend server listening on {bind}");
- if with_api {
- println!("API routes available at /api/*");
- }
- axum::serve(listener, app)
- .await
- .expect("frontend server error");
-}
diff --git a/src/server/mod.rs b/src/server/mod.rs
new file mode 100644
index 0000000..d9e39f1
--- /dev/null
+++ b/src/server/mod.rs
@@ -0,0 +1,46 @@
+use axum::Router;
+use leptos::config::LeptosOptions;
+use leptos_axum::{LeptosRoutes, generate_route_list};
+
+use crate::app::{App, shell};
+
+/// Build the server router with Leptos SSR.
+///
+/// When `with_api` is true, the API routes are nested under `/api`.
+pub fn router(leptos_options: LeptosOptions, with_api: bool) -> Router {
+ let routes = generate_route_list(App);
+
+ let mut app = Router::new();
+
+ if with_api {
+ app = app.nest_service("/api", crate::api::router());
+ }
+
+ app.leptos_routes(&leptos_options, routes, {
+ let leptos_options = leptos_options.clone();
+ move || shell(leptos_options.clone())
+ })
+ .fallback(leptos_axum::file_and_error_handler::(
+ shell,
+ ))
+ .with_state(leptos_options)
+}
+
+/// Start the frontend server with Leptos SSR.
+///
+/// If `with_api` is true, the API routes are nested under `/api`.
+pub async fn serve(bind: &str, with_api: bool) {
+ let conf = leptos::config::get_configuration(Some("Cargo.toml")).unwrap();
+ let leptos_options = conf.leptos_options;
+
+ let app = router(leptos_options, with_api);
+
+ let listener = tokio::net::TcpListener::bind(bind)
+ .await
+ .expect("failed to bind server");
+ println!("Server listening on {bind}");
+ if with_api {
+ println!("API routes available at /api/*");
+ }
+ axum::serve(listener, app).await.expect("server error");
+}
diff --git a/tests/server.rs b/tests/server.rs
index e89a4bb..0ca11d1 100644
--- a/tests/server.rs
+++ b/tests/server.rs
@@ -1,27 +1,15 @@
use axum::body::Body;
use axum::http::{Request, StatusCode};
-use http_body_util::BodyExt;
+use leptos::config::get_configuration;
use tower::ServiceExt;
use crate::*;
-#[tokio::test]
-async fn get_index_returns_placeholder() {
- let app = entangle::server::router();
- let response = app
- .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
- .await
- .unwrap();
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let body = response.into_body().collect().await.unwrap().to_bytes();
- assert_eq!(&body[..], b"entangle frontend - coming soon");
-}
-
#[tokio::test]
async fn with_api_nesting() {
- let app = entangle::server::router().nest("/api", entangle::api::router());
+ let conf = get_configuration(None).unwrap();
+ let leptos_options = conf.leptos_options;
+ let app = entangle::server::router(leptos_options, true);
let response = app
.oneshot(
Request::builder()
From 3d5df00a304acde4c573b638d1e36ea50657b1d9 Mon Sep 17 00:00:00 2001
From: parabit
Date: Sun, 15 Feb 2026 22:19:42 -0600
Subject: [PATCH 3/7] layout and build improvements
---
.gitignore | 1 +
Cargo.toml | 44 +++++++++++++++++++-----------
justfile | 7 ++++-
src/app.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++----
src/lib.rs | 20 ++++++++------
5 files changed, 121 insertions(+), 31 deletions(-)
diff --git a/.gitignore b/.gitignore
index 9d49729..9505327 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@
/.hermit
/.idea
+.env.local
justfile.local
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index d35b174..10df90b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,40 +37,51 @@ tower = { version = "0.5", features = ["util"] }
wasm-bindgen = "0.2"
[features]
-default = ["ssr"]
-ssr = [
+default = ["server"]
+api = [
"dep:axum",
"dep:bitcoin",
"dep:clap",
- "dep:leptos_axum",
"dep:serde",
"dep:serde_json",
"dep:tokio",
- "leptos/ssr",
- "leptos_meta/ssr",
- "leptos_router/ssr",
]
hydrate = [
"dep:console_error_panic_hook",
+ "dep:leptos",
+ "dep:leptos_meta",
+ "dep:leptos_router",
"dep:wasm-bindgen",
"leptos/hydrate",
]
+server = [
+ "api",
+ "dep:leptos",
+ "dep:leptos_axum",
+ "dep:leptos_meta",
+ "dep:leptos_router",
+ "leptos/ssr",
+ "leptos_meta/ssr",
+ "leptos_router/ssr",
+]
[dependencies]
-# Shared (compiled for both SSR and hydrate)
-leptos = { workspace = true }
-leptos_meta = { workspace = true }
-leptos_router = { workspace = true }
-
-# SSR-only
+# API + swap core (enabled by api feature)
axum = { workspace = true, optional = true }
bitcoin = { workspace = true, optional = true }
clap = { workspace = true, optional = true }
-leptos_axum = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
+# Leptos (enabled by server or hydrate features)
+leptos = { workspace = true, optional = true }
+leptos_meta = { workspace = true, optional = true }
+leptos_router = { workspace = true, optional = true }
+
+# Server-only
+leptos_axum = { workspace = true, optional = true }
+
# Hydrate-only
console_error_panic_hook = { workspace = true, optional = true }
wasm-bindgen = { workspace = true, optional = true }
@@ -82,7 +93,7 @@ tower.workspace = true
[[bin]]
name = "entangle"
path = "src/bin/main.rs"
-required-features = ["ssr"]
+required-features = ["api"]
[lib]
name = "entangle"
@@ -92,7 +103,7 @@ crate-type = ["cdylib", "rlib"]
[[test]]
name = "integration"
path = "tests/lib.rs"
-required-features = ["ssr"]
+required-features = ["server"]
[package.metadata.leptos]
output-name = "entangle"
@@ -100,10 +111,11 @@ site-root = "target/site"
site-pkg-dir = "pkg"
site-addr = "127.0.0.1:8080"
reload-port = 3001
-bin-features = ["ssr"]
+bin-features = ["server"]
bin-default-features = false
lib-features = ["hydrate"]
lib-default-features = false
+assets-dir = "public"
lib-profile-release = "wasm-release"
[profile.wasm-release]
diff --git a/justfile b/justfile
index 1c13fa8..3fc403d 100644
--- a/justfile
+++ b/justfile
@@ -4,13 +4,18 @@ set dotenv-filename := ".env.local"
import? 'justfile.local'
dev:
- cargo run -- server
+ cargo leptos watch server
+
+serve:
+ cargo leptos serve server
init:
hermit init --quiet
hermit install just
hermit install rustup
rustup default stable
+ rustup target add wasm32-unknown-unknown
+ cargo install cargo-leptos
cargo clean
cargo build
diff --git a/src/app.rs b/src/app.rs
index f82ae44..ad9dba6 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,12 +1,12 @@
use leptos::prelude::*;
-use leptos_meta::MetaTags;
+use leptos_meta::{Link, MetaTags, Title};
use leptos_router::{
StaticSegment,
components::{Route, Router, Routes},
};
/// HTML shell wrapping the App. Used server-side to render the full document.
-#[cfg(feature = "ssr")]
+#[cfg(feature = "server")]
pub fn shell(options: leptos::config::LeptosOptions) -> impl IntoView {
view! {
@@ -18,7 +18,7 @@ pub fn shell(options: leptos::config::LeptosOptions) -> impl IntoView {
-
+
@@ -31,16 +31,86 @@ pub fn App() -> impl IntoView {
leptos_meta::provide_meta_context();
view! {
+
+
-
+
"Page not found" }>
-
+
}
}
+/// Page layout with top nav bar, slide-out sidebar, and main content area.
+#[component]
+fn Layout(children: Children) -> impl IntoView {
+ let sidebar_open = RwSignal::new(false);
+
+ view! {
+
+
+
+ {children()}
+
+ }
+}
+
+/// Top navigation bar.
+#[component]
+fn NavBar(sidebar_open: RwSignal) -> impl IntoView {
+ let toggle = move |_| sidebar_open.update(|open| *open = !*open);
+
+ view! {
+
+ }
+}
+
+/// Slide-out sidebar panel.
+#[component]
+fn Sidebar(sidebar_open: RwSignal) -> impl IntoView {
+ let style = move || {
+ let translate = if sidebar_open.get() {
+ "translateX(0%)"
+ } else {
+ "translateX(-100%)"
+ };
+ format!(
+ "position: fixed; top: 48px; left: 0; bottom: 0; width: 240px; \
+ background: #16213e; z-index: 90; \
+ transition: transform 0.2s ease; \
+ transform: {translate}; \
+ box-shadow: 2px 0 8px rgba(0,0,0,0.15);"
+ )
+ };
+
+ view! {
+
+ }
+}
+
#[component]
fn HomePage() -> impl IntoView {
view! {
diff --git a/src/lib.rs b/src/lib.rs
index ed996ee..c4c2bf2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,16 +1,16 @@
-pub mod app;
-
-#[cfg(feature = "ssr")]
+#[cfg(feature = "api")]
pub mod api;
-#[cfg(feature = "ssr")]
+#[cfg(any(feature = "server", feature = "hydrate"))]
+pub mod app;
+#[cfg(feature = "server")]
pub mod server;
-#[cfg(feature = "ssr")]
+#[cfg(feature = "api")]
pub mod swap;
-#[cfg(feature = "ssr")]
+#[cfg(feature = "api")]
use clap::{Parser, Subcommand};
-#[cfg(feature = "ssr")]
+#[cfg(feature = "api")]
#[derive(Parser)]
#[command(version, author, about = "Swaps made easy")]
pub struct Args {
@@ -18,7 +18,7 @@ pub struct Args {
pub command: Command,
}
-#[cfg(feature = "ssr")]
+#[cfg(feature = "api")]
#[derive(Subcommand)]
pub enum Command {
/// Start the REST API server.
@@ -28,6 +28,7 @@ pub enum Command {
bind: String,
},
/// Start the frontend web server.
+ #[cfg(feature = "server")]
Server {
/// Address to bind the frontend server to.
#[arg(long, default_value = "0.0.0.0:8080", env = "ENTANGLE_SERVER_BIND")]
@@ -38,7 +39,7 @@ pub enum Command {
},
}
-#[cfg(feature = "ssr")]
+#[cfg(feature = "api")]
pub async fn main() {
let args = Args::parse();
@@ -46,6 +47,7 @@ pub async fn main() {
Command::Api { bind } => {
api::serve(&bind).await;
}
+ #[cfg(feature = "server")]
Command::Server { bind, with_api } => {
server::serve(&bind, with_api).await;
}
From 61acb73a81787492c0806597e3e4eab84c797262 Mon Sep 17 00:00:00 2001
From: parabit
Date: Sun, 15 Feb 2026 22:20:14 -0600
Subject: [PATCH 4/7] add favicon
---
public/favicon.svg | 7 +++++++
1 file changed, 7 insertions(+)
create mode 100644 public/favicon.svg
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..a109b1d
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,7 @@
+
From 3d6ac3dc4490e882b919340334129ec269172f93 Mon Sep 17 00:00:00 2001
From: parabit
Date: Sun, 15 Feb 2026 22:42:35 -0600
Subject: [PATCH 5/7] apply theme
---
src/app.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 10 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index ad9dba6..1db94e1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -3,6 +3,7 @@ use leptos_meta::{Link, MetaTags, Title};
use leptos_router::{
StaticSegment,
components::{Route, Router, Routes},
+ hooks::use_location,
};
/// HTML shell wrapping the App. Used server-side to render the full document.
@@ -18,7 +19,8 @@ pub fn shell(options: leptos::config::LeptosOptions) -> impl IntoView {
-
+
@@ -37,6 +39,7 @@ pub fn App() -> impl IntoView {
"Page not found" }>
+
@@ -61,6 +64,8 @@ fn Layout(children: Children) -> impl IntoView {
#[component]
fn NavBar(sidebar_open: RwSignal) -> impl IntoView {
let toggle = move |_| sidebar_open.update(|open| *open = !*open);
+ let location = use_location();
+ let show_toggle = move || location.pathname.get() != "/" || sidebar_open.get();
view! {
}
}
+/// A navigation link that highlights when active.
+#[component]
+fn NavLink(href: &'static str, label: &'static str) -> impl IntoView {
+ let location = use_location();
+ let is_active = move || location.pathname.get() == href;
+
+ view! {
+
+ {label}
+
+ }
+}
+
/// Slide-out sidebar panel.
#[component]
fn Sidebar(sidebar_open: RwSignal) -> impl IntoView {
@@ -114,7 +139,17 @@ fn Sidebar(sidebar_open: RwSignal) -> impl IntoView {
#[component]
fn HomePage() -> impl IntoView {
view! {
- "entangle"
- "Swaps made easy"
+
+
"entangle"
+
"Swaps made easy"
+
+ }
+}
+
+#[component]
+fn ListingsPage() -> impl IntoView {
+ view! {
+ "Listings"
+ "No listings yet."
}
}
From c8ab42a103693b3eed00d3ceeeed5e9bdfddccba Mon Sep 17 00:00:00 2001
From: parabit
Date: Sun, 15 Feb 2026 22:46:10 -0600
Subject: [PATCH 6/7] adjust viewport when slide-out menu is open
---
src/app.rs | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/app.rs b/src/app.rs
index 1db94e1..abc1ed1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -54,7 +54,11 @@ fn Layout(children: Children) -> impl IntoView {
view! {
-
+
{children()}
}
From 10a883ba65e7bd28398358ce5705fd5758da8c77 Mon Sep 17 00:00:00 2001
From: parabit
Date: Mon, 16 Feb 2026 01:14:38 -0600
Subject: [PATCH 7/7] add listing form to sidebar
---
.gitignore | 3 +-
Cargo.lock | 383 ++++++++++++++++++++++++++++++++++++++++++++--
Cargo.toml | 4 +
src/app.rs | 213 ++++++++++++++++++++++++--
src/lib.rs | 2 +
src/listing.rs | 204 ++++++++++++++++++++++++
src/server/mod.rs | 7 +-
tests/lib.rs | 1 +
tests/listing.rs | 139 +++++++++++++++++
tests/server.rs | 8 +-
10 files changed, 942 insertions(+), 22 deletions(-)
create mode 100644 src/listing.rs
create mode 100644 tests/listing.rs
diff --git a/.gitignore b/.gitignore
index 9505327..c645ef2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@
/.idea
.env.local
-justfile.local
\ No newline at end of file
+justfile.local
+listings.json
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 3de8947..290344a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -343,6 +343,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
[[package]]
name = "clap"
version = "4.5.57"
@@ -618,6 +624,7 @@ dependencies = [
"leptos_axum",
"leptos_meta",
"leptos_router",
+ "reqwest",
"serde",
"serde_json",
"tokio",
@@ -777,6 +784,19 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -981,6 +1001,24 @@ dependencies = [
"pin-utils",
"smallvec",
"tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
]
[[package]]
@@ -989,13 +1027,21 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
+ "base64 0.22.1",
"bytes",
+ "futures-channel",
+ "futures-util",
"http",
"http-body",
"hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
"pin-project-lite",
+ "socket2",
"tokio",
"tower-service",
+ "tracing",
]
[[package]]
@@ -1133,6 +1179,22 @@ dependencies = [
"rustversion",
]
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -1425,6 +1487,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
[[package]]
name = "manyhow"
version = "0.11.4"
@@ -1700,6 +1768,61 @@ dependencies = [
"yansi",
]
+[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
[[package]]
name = "quote"
version = "1.0.44"
@@ -1858,6 +1981,58 @@ version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "rstml"
version = "0.12.1"
@@ -1888,6 +2063,41 @@ dependencies = [
"semver",
]
+[[package]]
+name = "rustls"
+version = "0.23.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -2178,6 +2388,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "syn"
version = "2.0.114"
@@ -2206,6 +2422,9 @@ name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
[[package]]
name = "synstructure"
@@ -2311,6 +2530,21 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "tinyvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
+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.49.0"
@@ -2337,6 +2571,16 @@ dependencies = [
"syn",
]
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
[[package]]
name = "tokio-tungstenite"
version = "0.28.0"
@@ -2424,12 +2668,14 @@ dependencies = [
"http-body-util",
"http-range-header",
"httpdate",
+ "iri-string",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"tokio",
"tokio-util",
+ "tower",
"tower-layer",
"tower-service",
"tracing",
@@ -2467,6 +2713,12 @@ 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.28.0"
@@ -2554,6 +2806,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+[[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.8"
@@ -2617,6 +2875,15 @@ dependencies = [
"winapi-util",
]
+[[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.1+wasi-snapshot-preview1"
@@ -2779,6 +3046,25 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -2794,13 +3080,22 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[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.6",
+]
+
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
- "windows-targets",
+ "windows-targets 0.53.5",
]
[[package]]
@@ -2812,6 +3107,22 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
[[package]]
name = "windows-targets"
version = "0.53.5"
@@ -2819,58 +3130,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
]
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
@@ -3056,6 +3415,12 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
[[package]]
name = "zerotrie"
version = "0.2.3"
diff --git a/Cargo.toml b/Cargo.toml
index 10df90b..d0303e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ leptos = "0.8"
leptos_axum = "0.8"
leptos_meta = "0.8"
leptos_router = "0.8"
+reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
@@ -42,6 +43,7 @@ api = [
"dep:axum",
"dep:bitcoin",
"dep:clap",
+ "dep:reqwest",
"dep:serde",
"dep:serde_json",
"dep:tokio",
@@ -51,6 +53,7 @@ hydrate = [
"dep:leptos",
"dep:leptos_meta",
"dep:leptos_router",
+ "dep:serde",
"dep:wasm-bindgen",
"leptos/hydrate",
]
@@ -70,6 +73,7 @@ server = [
axum = { workspace = true, optional = true }
bitcoin = { workspace = true, optional = true }
clap = { workspace = true, optional = true }
+reqwest = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
diff --git a/src/app.rs b/src/app.rs
index abc1ed1..849ecd8 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,11 +1,15 @@
use leptos::prelude::*;
-use leptos_meta::{Link, MetaTags, Title};
+#[cfg(feature = "server")]
+use leptos_meta::MetaTags;
+use leptos_meta::{Link, Title};
use leptos_router::{
StaticSegment,
components::{Route, Router, Routes},
hooks::use_location,
};
+use crate::listing::{CreateListing, Listing, list_listings};
+
/// HTML shell wrapping the App. Used server-side to render the full document.
#[cfg(feature = "server")]
pub fn shell(options: leptos::config::LeptosOptions) -> impl IntoView {
@@ -50,14 +54,16 @@ pub fn App() -> impl IntoView {
#[component]
fn Layout(children: Children) -> impl IntoView {
let sidebar_open = RwSignal::new(false);
+ let create_action = ServerAction::::new();
+ provide_context(create_action.version());
view! {
-
+
{children()}
@@ -115,9 +121,22 @@ fn NavLink(href: &'static str, label: &'static str) -> impl IntoView {
}
}
-/// Slide-out sidebar panel.
+const INPUT_STYLE: &str = "width: 100%; padding: 6px 8px; background: #1a1a2e; \
+ border: 1px solid #333; color: #e0e0e0; border-radius: 4px; \
+ font-size: 13px; box-sizing: border-box;";
+
+const LABEL_STYLE: &str = "display: block; font-size: 11px; color: #999; \
+ margin: 8px 0 2px 0;";
+
+/// Slide-out sidebar panel with listing creation form.
#[component]
-fn Sidebar(sidebar_open: RwSignal) -> impl IntoView {
+fn Sidebar(
+ sidebar_open: RwSignal,
+ create_action: ServerAction,
+) -> impl IntoView {
+ let pending = create_action.pending();
+ let value = create_action.value();
+
let style = move || {
let translate = if sidebar_open.get() {
"translateX(0%)"
@@ -125,17 +144,71 @@ fn Sidebar(sidebar_open: RwSignal) -> impl IntoView {
"translateX(-100%)"
};
format!(
- "position: fixed; top: 48px; left: 0; bottom: 0; width: 240px; \
+ "position: fixed; top: 48px; left: 0; bottom: 0; width: 280px; \
background: #16213e; z-index: 90; \
transition: transform 0.2s ease; \
transform: {translate}; \
- box-shadow: 2px 0 8px rgba(0,0,0,0.15);"
+ box-shadow: 2px 0 8px rgba(0,0,0,0.15); \
+ overflow-y: auto;"
)
};
view! {
}
}
@@ -152,8 +225,130 @@ fn HomePage() -> impl IntoView {
#[component]
fn ListingsPage() -> impl IntoView {
+ let search = RwSignal::new(String::new());
+ let version = expect_context::>();
+
+ let listings = Resource::new(
+ move || (search.get(), version.get()),
+ |(s, _)| list_listings(s),
+ );
+
view! {
"Listings"
- "No listings yet."
+
+ "Loading listings..." }>
+ {move || Suspend::new(async move {
+ match listings.await {
+ Ok(items) if items.is_empty() => {
+ view! { "No listings found."
}.into_any()
+ }
+ Ok(items) => {
+ items
+ .into_iter()
+ .map(|l| view! { })
+ .collect_view()
+ .into_any()
+ }
+ Err(e) => {
+ view! {
+ "Error: " {e.to_string()}
+ }
+ .into_any()
+ }
+ }
+ })}
+
+ }
+}
+
+#[component]
+fn ListingCard(listing: Listing) -> impl IntoView {
+ let Listing {
+ id,
+ utxo_a,
+ amount_a_sats,
+ address_a,
+ utxo_b,
+ amount_b_sats,
+ address_b,
+ network,
+ tags,
+ ..
+ } = listing;
+
+ let utxo_b_view = utxo_b.map(|u| {
+ view! {
+
+ "UTXO B: " {u}
+
+ }
+ });
+ let amount_b_view = amount_b_sats.map(|a| {
+ view! {
+
+ "Amount B: " {a} " sats"
+
+ }
+ });
+ let tags_view = (!tags.is_empty()).then(|| {
+ let chips = tags
+ .into_iter()
+ .map(|t| {
+ view! {
+
+ {t}
+
+ }
+ })
+ .collect_view();
+ view! {
+
+ {chips}
+
+ }
+ });
+
+ view! {
+
+
+
+ "#" {id}
+
+
+ {network}
+
+
+
+
+ "UTXO A: " {utxo_a}
+
+
+ "Amount A: " {amount_a_sats} " sats"
+
+
+ "Address A: "
+ {address_a}
+
+ {utxo_b_view}
+ {amount_b_view}
+
+ "Address B: "
+ {address_b}
+
+
+ {tags_view}
+
}
}
diff --git a/src/lib.rs b/src/lib.rs
index c4c2bf2..9db3f28 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,8 @@
pub mod api;
#[cfg(any(feature = "server", feature = "hydrate"))]
pub mod app;
+#[cfg(any(feature = "server", feature = "hydrate"))]
+pub mod listing;
#[cfg(feature = "server")]
pub mod server;
#[cfg(feature = "api")]
diff --git a/src/listing.rs b/src/listing.rs
new file mode 100644
index 0000000..3e8199c
--- /dev/null
+++ b/src/listing.rs
@@ -0,0 +1,204 @@
+use leptos::prelude::*;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Listing {
+ pub id: u64,
+ pub utxo_a: String,
+ pub amount_a_sats: u64,
+ pub address_a: String,
+ pub utxo_b: Option,
+ pub amount_b_sats: Option,
+ pub address_b: String,
+ pub network: String,
+ pub tags: Vec,
+ pub created_at: u64,
+ #[serde(default)]
+ pub script_pubkey_a: Option,
+ #[serde(default)]
+ pub script_pubkey_b: Option,
+}
+
+#[cfg(feature = "server")]
+mod store {
+ use super::Listing;
+ use leptos::prelude::ServerFnError;
+ use std::path::PathBuf;
+ use std::sync::Arc;
+ use tokio::sync::RwLock;
+
+ #[derive(serde::Deserialize)]
+ struct EsploraTransaction {
+ vout: Vec,
+ }
+
+ #[derive(serde::Deserialize)]
+ struct EsploraVout {
+ scriptpubkey: String,
+ value: u64,
+ }
+
+ fn esplora_base_url(network: &str) -> String {
+ if let Ok(url) = std::env::var("ENTANGLE_ESPLORA_URL") {
+ return url;
+ }
+ match network {
+ "mainnet" => "https://mempool.space/api".into(),
+ "testnet" | "testnet4" => "https://mempool.space/testnet4/api".into(),
+ "signet" => "https://mempool.space/signet/api".into(),
+ _ => "https://mempool.space/testnet4/api".into(),
+ }
+ }
+
+ pub async fn fetch_utxo(network: &str, outpoint: &str) -> Result<(u64, String), ServerFnError> {
+ let (txid, vout_str) = outpoint
+ .rsplit_once(':')
+ .ok_or_else(|| ServerFnError::new("invalid outpoint format, expected txid:vout"))?;
+ let vout_idx: usize = vout_str
+ .parse()
+ .map_err(|_| ServerFnError::new("invalid vout index"))?;
+
+ let base = esplora_base_url(network);
+ let url = format!("{base}/tx/{txid}");
+
+ let tx: EsploraTransaction = reqwest::get(&url)
+ .await
+ .map_err(|e| ServerFnError::new(format!("esplora request failed: {e}")))?
+ .json()
+ .await
+ .map_err(|e| ServerFnError::new(format!("esplora response parse failed: {e}")))?;
+
+ let output = tx
+ .vout
+ .get(vout_idx)
+ .ok_or_else(|| ServerFnError::new(format!("vout index {vout_idx} out of range")))?;
+
+ Ok((output.value, output.scriptpubkey.clone()))
+ }
+
+ struct Inner {
+ listings: RwLock>,
+ path: PathBuf,
+ }
+
+ #[derive(Clone)]
+ pub struct ListingStore {
+ inner: Arc,
+ }
+
+ impl ListingStore {
+ pub fn load(path: impl Into) -> Self {
+ let path = path.into();
+ let listings = if path.exists() {
+ let data = std::fs::read_to_string(&path).unwrap_or_default();
+ serde_json::from_str(&data).unwrap_or_default()
+ } else {
+ Vec::new()
+ };
+ ListingStore {
+ inner: Arc::new(Inner {
+ listings: RwLock::new(listings),
+ path,
+ }),
+ }
+ }
+
+ pub async fn create(&self, mut listing: Listing) -> Listing {
+ let mut listings = self.inner.listings.write().await;
+ listing.id = listings.iter().map(|l| l.id).max().unwrap_or(0) + 1;
+ listing.created_at = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+ listings.push(listing.clone());
+ self.save(&listings);
+ listing
+ }
+
+ pub async fn search(&self, query: &str) -> Vec {
+ let listings = self.inner.listings.read().await;
+ if query.is_empty() {
+ return listings.clone();
+ }
+ let q = query.to_lowercase();
+ listings
+ .iter()
+ .filter(|l| {
+ l.utxo_a.to_lowercase().contains(&q)
+ || l.address_a.to_lowercase().contains(&q)
+ || l.address_b.to_lowercase().contains(&q)
+ || l.utxo_b
+ .as_ref()
+ .is_some_and(|u| u.to_lowercase().contains(&q))
+ || l.network.to_lowercase().contains(&q)
+ || l.tags.iter().any(|t| t.to_lowercase().contains(&q))
+ })
+ .cloned()
+ .collect()
+ }
+
+ fn save(&self, listings: &[Listing]) {
+ if let Ok(data) = serde_json::to_string_pretty(listings) {
+ let _ = std::fs::write(&self.inner.path, data);
+ }
+ }
+ }
+}
+
+#[cfg(feature = "server")]
+pub use store::ListingStore;
+
+#[server]
+pub async fn create_listing(
+ utxo_a: String,
+ address_a: String,
+ utxo_b: String,
+ address_b: String,
+ network: String,
+ tags: String,
+) -> Result {
+ let axum::Extension(store): axum::Extension = leptos_axum::extract().await?;
+
+ let (amount_a_sats, script_pubkey_a) = store::fetch_utxo(&network, &utxo_a).await?;
+
+ let utxo_b = if utxo_b.trim().is_empty() {
+ None
+ } else {
+ Some(utxo_b)
+ };
+ let (amount_b_sats, script_pubkey_b) = if let Some(ref ub) = utxo_b {
+ let (amt, spk) = store::fetch_utxo(&network, ub).await?;
+ (Some(amt), Some(spk))
+ } else {
+ (None, None)
+ };
+
+ let tags: Vec = tags
+ .split(',')
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ .collect();
+
+ let listing = Listing {
+ id: 0,
+ utxo_a,
+ amount_a_sats,
+ address_a,
+ utxo_b,
+ amount_b_sats,
+ address_b,
+ network,
+ tags,
+ created_at: 0,
+ script_pubkey_a: Some(script_pubkey_a),
+ script_pubkey_b,
+ };
+
+ Ok(store.create(listing).await)
+}
+
+#[server]
+pub async fn list_listings(search: String) -> Result, ServerFnError> {
+ let axum::Extension(store): axum::Extension = leptos_axum::extract().await?;
+ Ok(store.search(&search).await)
+}
diff --git a/src/server/mod.rs b/src/server/mod.rs
index d9e39f1..e809def 100644
--- a/src/server/mod.rs
+++ b/src/server/mod.rs
@@ -3,11 +3,12 @@ use leptos::config::LeptosOptions;
use leptos_axum::{LeptosRoutes, generate_route_list};
use crate::app::{App, shell};
+use crate::listing::ListingStore;
/// Build the server router with Leptos SSR.
///
/// When `with_api` is true, the API routes are nested under `/api`.
-pub fn router(leptos_options: LeptosOptions, with_api: bool) -> Router {
+pub fn router(leptos_options: LeptosOptions, with_api: bool, store: ListingStore) -> Router {
let routes = generate_route_list(App);
let mut app = Router::new();
@@ -20,6 +21,7 @@ pub fn router(leptos_options: LeptosOptions, with_api: bool) -> Router {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
+ .layer(axum::Extension(store))
.fallback(leptos_axum::file_and_error_handler::(
shell,
))
@@ -32,8 +34,9 @@ pub fn router(leptos_options: LeptosOptions, with_api: bool) -> Router {
pub async fn serve(bind: &str, with_api: bool) {
let conf = leptos::config::get_configuration(Some("Cargo.toml")).unwrap();
let leptos_options = conf.leptos_options;
+ let store = ListingStore::load("listings.json");
- let app = router(leptos_options, with_api);
+ let app = router(leptos_options, with_api, store);
let listener = tokio::net::TcpListener::bind(bind)
.await
diff --git a/tests/lib.rs b/tests/lib.rs
index 4da0f41..9b635fa 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -1,4 +1,5 @@
mod api;
+mod listing;
mod server;
mod swap;
diff --git a/tests/listing.rs b/tests/listing.rs
new file mode 100644
index 0000000..7377ca6
--- /dev/null
+++ b/tests/listing.rs
@@ -0,0 +1,139 @@
+pub(crate) use entangle::listing::{Listing, ListingStore};
+
+fn temp_path(name: &str) -> std::path::PathBuf {
+ std::env::temp_dir().join(format!("entangle_test_{name}.json"))
+}
+
+fn make_listing() -> Listing {
+ Listing {
+ id: 0,
+ utxo_a: "aabb:0".into(),
+ amount_a_sats: 50_000,
+ address_a: "tb1qaddr_a".into(),
+ utxo_b: None,
+ amount_b_sats: None,
+ address_b: "tb1qaddr_b".into(),
+ network: "testnet".into(),
+ tags: vec!["swap".into()],
+ created_at: 0,
+ script_pubkey_a: None,
+ script_pubkey_b: None,
+ }
+}
+
+#[tokio::test]
+async fn create_and_search() {
+ let path = temp_path("create_search");
+ let _ = std::fs::remove_file(&path);
+
+ let store = ListingStore::load(&path);
+ let created = store.create(make_listing()).await;
+
+ assert_eq!(created.id, 1);
+ assert!(created.created_at > 0);
+
+ let results = store.search("aabb").await;
+ assert_eq!(results.len(), 1);
+ assert_eq!(results[0].utxo_a, "aabb:0");
+
+ let results = store.search("nonexistent").await;
+ assert!(results.is_empty());
+
+ let _ = std::fs::remove_file(&path);
+}
+
+#[tokio::test]
+async fn persistence_across_loads() {
+ let path = temp_path("persistence");
+ let _ = std::fs::remove_file(&path);
+
+ {
+ let store = ListingStore::load(&path);
+ store.create(make_listing()).await;
+ }
+
+ let store = ListingStore::load(&path);
+ let results = store.search("").await;
+ assert_eq!(results.len(), 1);
+ assert_eq!(results[0].id, 1);
+
+ let _ = std::fs::remove_file(&path);
+}
+
+#[tokio::test]
+async fn case_insensitive_search() {
+ let path = temp_path("case_insensitive");
+ let _ = std::fs::remove_file(&path);
+
+ let store = ListingStore::load(&path);
+ let mut listing = make_listing();
+ listing.utxo_a = "AABB:0".into();
+ listing.tags = vec!["Atomic".into()];
+ store.create(listing).await;
+
+ let results = store.search("aabb").await;
+ assert_eq!(results.len(), 1);
+
+ let results = store.search("atomic").await;
+ assert_eq!(results.len(), 1);
+
+ let results = store.search("AABB").await;
+ assert_eq!(results.len(), 1);
+
+ let _ = std::fs::remove_file(&path);
+}
+
+#[tokio::test]
+async fn search_matches_address_and_tags() {
+ let path = temp_path("search_fields");
+ let _ = std::fs::remove_file(&path);
+
+ let store = ListingStore::load(&path);
+ store.create(make_listing()).await;
+
+ // Search by address_a
+ assert_eq!(store.search("addr_a").await.len(), 1);
+ // Search by address_b
+ assert_eq!(store.search("addr_b").await.len(), 1);
+ // Search by tag
+ assert_eq!(store.search("swap").await.len(), 1);
+ // Search by network
+ assert_eq!(store.search("testnet").await.len(), 1);
+
+ let _ = std::fs::remove_file(&path);
+}
+
+#[tokio::test]
+async fn empty_search_returns_all() {
+ let path = temp_path("empty_search");
+ let _ = std::fs::remove_file(&path);
+
+ let store = ListingStore::load(&path);
+ store.create(make_listing()).await;
+
+ let mut listing2 = make_listing();
+ listing2.utxo_a = "ccdd:1".into();
+ store.create(listing2).await;
+
+ let results = store.search("").await;
+ assert_eq!(results.len(), 2);
+
+ let _ = std::fs::remove_file(&path);
+}
+
+#[tokio::test]
+async fn ids_auto_increment() {
+ let path = temp_path("auto_increment");
+ let _ = std::fs::remove_file(&path);
+
+ let store = ListingStore::load(&path);
+ let a = store.create(make_listing()).await;
+ let b = store.create(make_listing()).await;
+ let c = store.create(make_listing()).await;
+
+ assert_eq!(a.id, 1);
+ assert_eq!(b.id, 2);
+ assert_eq!(c.id, 3);
+
+ let _ = std::fs::remove_file(&path);
+}
diff --git a/tests/server.rs b/tests/server.rs
index 0ca11d1..357272e 100644
--- a/tests/server.rs
+++ b/tests/server.rs
@@ -5,11 +5,17 @@ use tower::ServiceExt;
use crate::*;
+fn temp_store() -> listing::ListingStore {
+ let path = std::env::temp_dir().join("entangle_test_server.json");
+ let _ = std::fs::remove_file(&path);
+ listing::ListingStore::load(path)
+}
+
#[tokio::test]
async fn with_api_nesting() {
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
- let app = entangle::server::router(leptos_options, true);
+ let app = entangle::server::router(leptos_options, true, temp_store());
let response = app
.oneshot(
Request::builder()