feat(ecash): adopt the reference NUT-02 resolver + real/test network switch
Demo images / Build & push demo images (push) Failing after 2m26s
Demo images / Build & push demo images (push) Failing after 2m26s
Executes steps 1-3 of docs/cashu-cdk-migration-plan.md, plus the test-coin
switch needed to exercise these routes without spending real sats.
Protocol layer: depend on `cashu` 0.17.5 (MIT, the crate CDK is built on,
default-features off, `wallet` only). Keyset ids now go through upstream's
`Id::from_short_keyset_id` / `ShortKeysetId` instead of the prefix match
hand-rolled in 2277fc46 — same repair, but implemented by the reference
code that defines the rule, so the next spec turn is a version bump rather
than another incident. `MintClient` feeds it the mint's `/v1/keysets` in
upstream's own `KeySetInfo` shape, parsing entries individually so one
keyset in an unmodelled unit can't block resolving the id we need.
Adding the crate required relaxing `bip39 = "=2.1.0"` to `"2.1"` (resolves
2.2.2): the exact pin held `unicode-normalization` at 0.1.22 and no
resolution existed otherwise. The pin carried no recorded rationale; seed
tests cover the bump.
Network switch: `wallet.ecash-network` / `wallet.ecash-set-network`, with a
Test mode toggle in Wallet Settings → Cashu. Cashu has no testnet, so this
points the wallet at the public `testnut` mint — but crucially each network
gets its OWN wallet and accepted-mints file, because test and real proofs
in one purse would be spendable interchangeably and the balance would be a
lie. Mainnet keeps the original filenames, so existing funds files are
untouched and switching is reversible: tests assert a real balance survives
a round trip through test mode.
Headless coverage: scripts/test-ecash-routes.sh drives every ecash RPC over
the real HTTP path (network get/set, balance, history, mint quote + claim,
send, receive, double-redeem refusal, garbage input, melt quote), restores
the node's original network on exit, and exits non-zero with the failure
count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
03cf74696d
commit
be2cfb8293
Generated
+397
-57
@@ -120,6 +120,7 @@ dependencies = [
|
|||||||
"blake3",
|
"blake3",
|
||||||
"bs58",
|
"bs58",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cashu",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"chrono",
|
"chrono",
|
||||||
"ciborium",
|
"ciborium",
|
||||||
@@ -186,7 +187,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
"hyper 0.14.32",
|
"hyper 0.14.32",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"log",
|
"log",
|
||||||
"reqwest 0.11.27",
|
"reqwest 0.11.27",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -450,8 +451,8 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
|
checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin-internals 0.3.0",
|
"bitcoin-internals",
|
||||||
"bitcoin_hashes 0.14.1",
|
"bitcoin_hashes",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -499,11 +500,11 @@ checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bip39"
|
name = "bip39"
|
||||||
version = "2.1.0"
|
version = "2.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387"
|
checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin_hashes 0.13.0",
|
"bitcoin_hashes",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -526,27 +527,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026"
|
checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base58ck",
|
"base58ck",
|
||||||
|
"base64 0.21.7",
|
||||||
"bech32",
|
"bech32",
|
||||||
"bitcoin-internals 0.3.0",
|
"bitcoin-internals",
|
||||||
"bitcoin-io",
|
"bitcoin-io",
|
||||||
"bitcoin-units",
|
"bitcoin-units",
|
||||||
"bitcoin_hashes 0.14.1",
|
"bitcoin_hashes",
|
||||||
"hex-conservative 0.2.2",
|
"hex-conservative",
|
||||||
"hex_lit",
|
"hex_lit",
|
||||||
"secp256k1",
|
"secp256k1",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitcoin-internals"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin-internals"
|
name = "bitcoin-internals"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
|
checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin-io"
|
name = "bitcoin-io"
|
||||||
@@ -560,17 +560,8 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2"
|
checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin-internals 0.3.0",
|
"bitcoin-internals",
|
||||||
]
|
"serde",
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitcoin_hashes"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
|
|
||||||
dependencies = [
|
|
||||||
"bitcoin-internals 0.2.0",
|
|
||||||
"hex-conservative 0.1.2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -580,7 +571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
|
checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin-io",
|
"bitcoin-io",
|
||||||
"hex-conservative 0.2.2",
|
"hex-conservative",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -707,6 +698,32 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cashu"
|
||||||
|
version = "0.17.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8bd7216af2b980e203d10677076d8c6c5c30610cbdea6549b8a9020cfa0cf47b"
|
||||||
|
dependencies = [
|
||||||
|
"bitcoin",
|
||||||
|
"cbor-diag",
|
||||||
|
"ciborium",
|
||||||
|
"lightning",
|
||||||
|
"lightning-invoice",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
|
"strum 0.27.2",
|
||||||
|
"strum_macros 0.27.2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tracing",
|
||||||
|
"unicode-normalization",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"web-time",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cbc"
|
name = "cbc"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -716,6 +733,25 @@ dependencies = [
|
|||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cbor-diag"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc245b6ecd09b23901a4fbad1ad975701fd5061ceaef6afa93a2d70605a64429"
|
||||||
|
dependencies = [
|
||||||
|
"bs58",
|
||||||
|
"chrono",
|
||||||
|
"data-encoding",
|
||||||
|
"half",
|
||||||
|
"nom",
|
||||||
|
"num-bigint",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
|
"separator",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.54"
|
version = "1.2.54"
|
||||||
@@ -1092,8 +1128,18 @@ version = "0.20.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.20.11",
|
||||||
"darling_macro",
|
"darling_macro 0.20.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.23.0",
|
||||||
|
"darling_macro 0.23.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1110,13 +1156,37 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||||
|
dependencies = [
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.20.11"
|
version = "0.20.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.20.11",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.23.0",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
@@ -1147,6 +1217,37 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"defmt-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt-macros"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
|
||||||
|
dependencies = [
|
||||||
|
"defmt-parser",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt-parser"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@@ -1187,6 +1288,9 @@ name = "deranged"
|
|||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_arbitrary"
|
name = "derive_arbitrary"
|
||||||
@@ -1214,7 +1318,7 @@ version = "0.20.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.20.11",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
@@ -1314,6 +1418,18 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dnssec-prover"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "869bf72abc8c654b350aa8d881c5d9957b85e1e1ed6569c10cd68e9f505d5435"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ed25519"
|
name = "ed25519"
|
||||||
version = "2.2.3"
|
version = "2.2.3"
|
||||||
@@ -1774,7 +1890,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -1793,7 +1909,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http 1.4.0",
|
"http 1.4.0",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -1829,6 +1945,12 @@ dependencies = [
|
|||||||
"ahash",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -1887,12 +2009,6 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hex-conservative"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex-conservative"
|
name = "hex-conservative"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -2391,6 +2507,17 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -2507,7 +2634,7 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"strum",
|
"strum 0.28.0",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
@@ -2595,7 +2722,7 @@ dependencies = [
|
|||||||
"rand 0.10.1",
|
"rand 0.10.1",
|
||||||
"rustls 0.23.36",
|
"rustls 0.23.36",
|
||||||
"simple-dns",
|
"simple-dns",
|
||||||
"strum",
|
"strum 0.28.0",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
@@ -2675,7 +2802,7 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"strum",
|
"strum 0.28.0",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.26.4",
|
"tokio-rustls 0.26.4",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -2765,6 +2892,59 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff"
|
||||||
|
version = "0.2.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
|
||||||
|
dependencies = [
|
||||||
|
"defmt",
|
||||||
|
"jiff-core",
|
||||||
|
"jiff-static",
|
||||||
|
"jiff-tzdb-platform",
|
||||||
|
"log",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"serde_core",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
|
||||||
|
dependencies = [
|
||||||
|
"defmt",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-static"
|
||||||
|
version = "0.2.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-core",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-tzdb"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-tzdb-platform"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-tzdb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -2911,6 +3091,55 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lightning"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ab16d2a714c0b26d7230bd388ac383a30fce231c8927c62752afc0471a36dc6"
|
||||||
|
dependencies = [
|
||||||
|
"bech32",
|
||||||
|
"bitcoin",
|
||||||
|
"dnssec-prover",
|
||||||
|
"hashbrown 0.13.2",
|
||||||
|
"libm",
|
||||||
|
"lightning-invoice",
|
||||||
|
"lightning-macros",
|
||||||
|
"lightning-types",
|
||||||
|
"possiblyrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lightning-invoice"
|
||||||
|
version = "0.34.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47d83bd798e04ab9eecc8bbef1fa17d3808859bcdc0406bd16c55d51c8834444"
|
||||||
|
dependencies = [
|
||||||
|
"bech32",
|
||||||
|
"bitcoin",
|
||||||
|
"lightning-types",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lightning-macros"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4c717494cdc2c8bb85bee7113031248f5f6c64f8802b33c1c9e2d98e594aa71"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lightning-types"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c77c676d4a34cceb2ae3756916e446b4d17f9430a24107e099981f0f9aec77e6"
|
||||||
|
dependencies = [
|
||||||
|
"bitcoin",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -3415,7 +3644,7 @@ dependencies = [
|
|||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bech32",
|
"bech32",
|
||||||
"bip39",
|
"bip39",
|
||||||
"bitcoin_hashes 0.14.1",
|
"bitcoin_hashes",
|
||||||
"cbc",
|
"cbc",
|
||||||
"chacha20 0.9.1",
|
"chacha20 0.9.1",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
@@ -3517,6 +3746,17 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-rational"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -3896,7 +4136,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
|
checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
@@ -3934,6 +4174,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portmapper"
|
name = "portmapper"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
@@ -3973,6 +4222,15 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "possiblyrandom"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c564dbf654befd49035528299f1208a40508f6e07efb11c163444e304e4484f"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "postcard"
|
name = "postcard"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -4643,6 +4901,30 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"ref-cast",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"ref-cast",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -4692,7 +4974,7 @@ version = "0.29.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin_hashes 0.14.1",
|
"bitcoin_hashes",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"secp256k1-sys",
|
"secp256k1-sys",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4758,6 +5040,12 @@ version = "0.6.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "separator"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -4842,13 +5130,46 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bs58",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"jiff",
|
||||||
|
"schemars 0.9.0",
|
||||||
|
"schemars 1.2.2",
|
||||||
|
"serde_core",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.23.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_yaml"
|
name = "serde_yaml"
|
||||||
version = "0.9.34+deprecated"
|
version = "0.9.34+deprecated"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5137,13 +5458,31 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"strum_macros",
|
"strum_macros 0.28.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5606,7 +5945,7 @@ version = "0.22.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime 0.6.11",
|
"toml_datetime 0.6.11",
|
||||||
@@ -5620,7 +5959,7 @@ version = "0.25.12+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"toml_datetime 1.1.1+spec-1.1.0",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow 1.0.3",
|
"winnow 1.0.3",
|
||||||
@@ -5823,9 +6162,9 @@ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
version = "0.1.22"
|
version = "0.1.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
@@ -5903,6 +6242,7 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"serde_core",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6080,7 +6420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"wasm-encoder",
|
"wasm-encoder",
|
||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
@@ -6119,7 +6459,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6678,7 +7018,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"heck",
|
"heck",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
"wasm-metadata",
|
"wasm-metadata",
|
||||||
@@ -6709,7 +7049,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@@ -6728,7 +7068,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"id-arena",
|
"id-arena",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"log",
|
"log",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -6959,7 +7299,7 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"flate2",
|
"flate2",
|
||||||
"indexmap",
|
"indexmap 2.13.0",
|
||||||
"memchr",
|
"memchr",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ bs58 = "0.5"
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
|
||||||
# BIP-39 mnemonic seed generation + BIP-32 HD key derivation
|
# BIP-39 mnemonic seed generation + BIP-32 HD key derivation
|
||||||
bip39 = { version = "=2.1.0", features = ["rand"] }
|
bip39 = { version = "2.1", features = ["rand"] }
|
||||||
bitcoin = { version = "=0.32.5", features = ["rand-std"] }
|
bitcoin = { version = "=0.32.5", features = ["rand-std"] }
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
@@ -143,6 +143,7 @@ async-trait = "0.1"
|
|||||||
iroh = { version = "1", optional = true }
|
iroh = { version = "1", optional = true }
|
||||||
iroh-blobs = { version = "0.103", optional = true }
|
iroh-blobs = { version = "0.103", optional = true }
|
||||||
lofty = "0.24.0"
|
lofty = "0.24.0"
|
||||||
|
cashu = { version = "0.17.5", default-features = false, features = ["wallet"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
|
|||||||
@@ -266,6 +266,8 @@ impl RpcHandler {
|
|||||||
"wallet.ecash-send" => self.handle_wallet_ecash_send(params).await,
|
"wallet.ecash-send" => self.handle_wallet_ecash_send(params).await,
|
||||||
"wallet.ecash-receive" => self.handle_wallet_ecash_receive(params).await,
|
"wallet.ecash-receive" => self.handle_wallet_ecash_receive(params).await,
|
||||||
"wallet.ecash-history" => self.handle_wallet_ecash_history().await,
|
"wallet.ecash-history" => self.handle_wallet_ecash_history().await,
|
||||||
|
"wallet.ecash-network" => self.handle_wallet_ecash_network().await,
|
||||||
|
"wallet.ecash-set-network" => self.handle_wallet_ecash_set_network(params).await,
|
||||||
"wallet.networking-profits" => self.handle_wallet_networking_profits().await,
|
"wallet.networking-profits" => self.handle_wallet_networking_profits().await,
|
||||||
// Fedimint ecash (via fedimint-clientd sidecar)
|
// Fedimint ecash (via fedimint-clientd sidecar)
|
||||||
"wallet.fedimint-list" => self.handle_wallet_fedimint_list().await,
|
"wallet.fedimint-list" => self.handle_wallet_fedimint_list().await,
|
||||||
|
|||||||
@@ -36,6 +36,51 @@ impl RpcHandler {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `wallet.ecash-network` — which ecash network this node is on, and the
|
||||||
|
/// balance sitting in the *other* one so the UI can say what switching
|
||||||
|
/// would reveal rather than appearing to lose money.
|
||||||
|
pub(super) async fn handle_wallet_ecash_network(&self) -> Result<serde_json::Value> {
|
||||||
|
let current = ecash::load_network(&self.config.data_dir).await;
|
||||||
|
let wallet = ecash::load_wallet(&self.config.data_dir).await?;
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"network": current,
|
||||||
|
"is_test": current.is_test(),
|
||||||
|
"mint_url": wallet.mint_url,
|
||||||
|
"balance_sats": wallet.balance(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `wallet.ecash-set-network` — switch between real and test ecash.
|
||||||
|
///
|
||||||
|
/// Each network keeps its own wallet file, so this never moves, merges or
|
||||||
|
/// deletes coins: switching away parks the current balance and switching
|
||||||
|
/// back finds it exactly as it was.
|
||||||
|
pub(super) async fn handle_wallet_ecash_set_network(
|
||||||
|
&self,
|
||||||
|
params: Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||||
|
let requested = params
|
||||||
|
.get("network")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing network ('mainnet' or 'testnet')"))?;
|
||||||
|
let network = match requested {
|
||||||
|
"mainnet" => ecash::EcashNetwork::Mainnet,
|
||||||
|
"testnet" => ecash::EcashNetwork::Testnet,
|
||||||
|
other => anyhow::bail!("Unknown ecash network '{other}' — use 'mainnet' or 'testnet'"),
|
||||||
|
};
|
||||||
|
|
||||||
|
ecash::save_network(&self.config.data_dir, network).await?;
|
||||||
|
let wallet = ecash::load_wallet(&self.config.data_dir).await?;
|
||||||
|
tracing::info!(?network, "ecash network switched by operator");
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"network": network,
|
||||||
|
"is_test": network.is_test(),
|
||||||
|
"mint_url": wallet.mint_url,
|
||||||
|
"balance_sats": wallet.balance(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_wallet_ecash_mint(
|
pub(super) async fn handle_wallet_ecash_mint(
|
||||||
&self,
|
&self,
|
||||||
params: Option<serde_json::Value>,
|
params: Option<serde_json::Value>,
|
||||||
|
|||||||
@@ -20,6 +20,13 @@
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bitcoin::secp256k1::PublicKey;
|
use bitcoin::secp256k1::PublicKey;
|
||||||
|
// Protocol types from the reference implementation (`cashu`, the crate CDK
|
||||||
|
// itself is built on). Used for the parts of NUT-00/02 that move with the
|
||||||
|
// spec — token parsing and keyset ids — while the structs below stay ours
|
||||||
|
// because they are also the on-disk format (see docs/cashu-cdk-migration-plan.md).
|
||||||
|
use cashu::nuts::nut02::{
|
||||||
|
Id as CdkId, KeySetInfo as CdkKeySetInfo, ShortKeysetId as CdkShortKeysetId,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Prefix for V3 (JSON) tokens.
|
/// Prefix for V3 (JSON) tokens.
|
||||||
@@ -222,6 +229,26 @@ impl CashuToken {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a token's (possibly short) keyset id against the mint's keyset
|
||||||
|
/// list, using the reference implementation's NUT-02 rules.
|
||||||
|
///
|
||||||
|
/// A v1 id is complete at 8 bytes; a v2 id is 33 bytes and may legitimately
|
||||||
|
/// travel in a token as a shorter prefix, which only the mint's keyset list
|
||||||
|
/// can expand. Upstream `Id::from_short_keyset_id` implements exactly that,
|
||||||
|
/// including the "8 bytes but `0x01`-versioned" case that a wallet written
|
||||||
|
/// against the old format produces (framework-pt, 2026-08-17).
|
||||||
|
///
|
||||||
|
/// Returns the full hex id to send to the mint, or `None` when the id is
|
||||||
|
/// already complete or cannot be resolved — the caller passes those through
|
||||||
|
/// untouched so the mint's own error still reaches the operator.
|
||||||
|
pub fn resolve_keyset_id(id_hex: &str, mint_keysets: &[CdkKeySetInfo]) -> Option<String> {
|
||||||
|
let bytes = hex::decode(id_hex).ok()?;
|
||||||
|
let short = CdkShortKeysetId::from_bytes(&bytes).ok()?;
|
||||||
|
let full = CdkId::from_short_keyset_id(&short, mint_keysets).ok()?;
|
||||||
|
let full_hex = hex::encode(full.to_bytes());
|
||||||
|
(full_hex != id_hex).then_some(full_hex)
|
||||||
|
}
|
||||||
|
|
||||||
/// NUT-02 keyset ID: hex for either 8 bytes (v1, the `00…` short form) or
|
/// NUT-02 keyset ID: hex for either 8 bytes (v1, the `00…` short form) or
|
||||||
/// 33 bytes (v2, version-byte + hash).
|
/// 33 bytes (v2, version-byte + hash).
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use tracing::{debug, info, warn};
|
|||||||
|
|
||||||
const WALLET_FILE: &str = "wallet/ecash.json";
|
const WALLET_FILE: &str = "wallet/ecash.json";
|
||||||
const MINTS_FILE: &str = "wallet/accepted_mints.json";
|
const MINTS_FILE: &str = "wallet/accepted_mints.json";
|
||||||
|
const NETWORK_FILE: &str = "wallet/network.json";
|
||||||
|
|
||||||
/// Transaction type for history tracking.
|
/// Transaction type for history tracking.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -213,12 +214,96 @@ impl WalletState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which ecash network this node's wallet is operating on.
|
||||||
|
///
|
||||||
|
/// Cashu itself has no notion of a testnet — a "test" wallet is simply one
|
||||||
|
/// pointed at a mint that issues valueless coins (the public `testnut` mint).
|
||||||
|
/// Modelling it as a network setting rather than "just add a mint" matters
|
||||||
|
/// because the two must never share a purse: test proofs and real proofs in
|
||||||
|
/// one file would be spendable interchangeably, and a balance would be a lie.
|
||||||
|
///
|
||||||
|
/// So each network gets its own wallet and its own accepted-mints list.
|
||||||
|
/// **Mainnet keeps the original filenames**, so an existing node's funds file
|
||||||
|
/// is untouched by this feature and by switching back and forth.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum EcashNetwork {
|
||||||
|
#[default]
|
||||||
|
Mainnet,
|
||||||
|
Testnet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EcashNetwork {
|
||||||
|
fn wallet_file(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mainnet => WALLET_FILE,
|
||||||
|
Self::Testnet => "wallet/ecash.testnet.json",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mints_file(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mainnet => MINTS_FILE,
|
||||||
|
Self::Testnet => "wallet/accepted_mints.testnet.json",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mint a fresh wallet on this network starts out trusting.
|
||||||
|
pub fn default_mint(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Mainnet => default_mint_url(),
|
||||||
|
// Public test mint: issues coins with no monetary value, and hands
|
||||||
|
// them out freely, so every route (mint/melt/send/receive/swap)
|
||||||
|
// can be exercised end to end without risking real sats.
|
||||||
|
Self::Testnet => "https://testnut.cashu.space".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_test(&self) -> bool {
|
||||||
|
matches!(self, Self::Testnet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the node's ecash network. Absent file = mainnet, so nodes that never
|
||||||
|
/// touch this setting behave exactly as before.
|
||||||
|
pub async fn load_network(data_dir: &Path) -> EcashNetwork {
|
||||||
|
let path = data_dir.join(NETWORK_FILE);
|
||||||
|
let Ok(content) = fs::read_to_string(&path).await else {
|
||||||
|
return EcashNetwork::Mainnet;
|
||||||
|
};
|
||||||
|
serde_json::from_str::<NetworkConfig>(&content)
|
||||||
|
.map(|c| c.network)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch the node's ecash network. The other network's wallet is left on
|
||||||
|
/// disk untouched, so switching is reversible and loses nothing.
|
||||||
|
pub async fn save_network(data_dir: &Path, network: EcashNetwork) -> Result<()> {
|
||||||
|
let dir = data_dir.join("wallet");
|
||||||
|
fs::create_dir_all(&dir)
|
||||||
|
.await
|
||||||
|
.context("Failed to create wallet dir")?;
|
||||||
|
let content = serde_json::to_string_pretty(&NetworkConfig { network })
|
||||||
|
.context("Failed to serialize ecash network")?;
|
||||||
|
fs::write(data_dir.join(NETWORK_FILE), content)
|
||||||
|
.await
|
||||||
|
.context("Failed to write ecash network")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||||
|
struct NetworkConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
network: EcashNetwork,
|
||||||
|
}
|
||||||
|
|
||||||
/// Load wallet state from disk.
|
/// Load wallet state from disk.
|
||||||
pub async fn load_wallet(data_dir: &Path) -> Result<WalletState> {
|
pub async fn load_wallet(data_dir: &Path) -> Result<WalletState> {
|
||||||
let path = data_dir.join(WALLET_FILE);
|
let network = load_network(data_dir).await;
|
||||||
|
let path = data_dir.join(network.wallet_file());
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Ok(WalletState {
|
return Ok(WalletState {
|
||||||
mint_url: default_mint_url(),
|
mint_url: network.default_mint(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -229,7 +314,7 @@ pub async fn load_wallet(data_dir: &Path) -> Result<WalletState> {
|
|||||||
|
|
||||||
// Set default mint URL if empty
|
// Set default mint URL if empty
|
||||||
if wallet.mint_url.is_empty() {
|
if wallet.mint_url.is_empty() {
|
||||||
wallet.mint_url = default_mint_url();
|
wallet.mint_url = network.default_mint();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(wallet)
|
Ok(wallet)
|
||||||
@@ -241,7 +326,7 @@ pub async fn save_wallet(data_dir: &Path, wallet: &WalletState) -> Result<()> {
|
|||||||
fs::create_dir_all(&dir)
|
fs::create_dir_all(&dir)
|
||||||
.await
|
.await
|
||||||
.context("Failed to create wallet dir")?;
|
.context("Failed to create wallet dir")?;
|
||||||
let path = data_dir.join(WALLET_FILE);
|
let path = data_dir.join(load_network(data_dir).await.wallet_file());
|
||||||
let content = serde_json::to_string_pretty(wallet).context("Failed to serialize wallet")?;
|
let content = serde_json::to_string_pretty(wallet).context("Failed to serialize wallet")?;
|
||||||
fs::write(&path, content)
|
fs::write(&path, content)
|
||||||
.await
|
.await
|
||||||
@@ -251,17 +336,18 @@ pub async fn save_wallet(data_dir: &Path, wallet: &WalletState) -> Result<()> {
|
|||||||
|
|
||||||
/// Load accepted mints list.
|
/// Load accepted mints list.
|
||||||
pub async fn load_accepted_mints(data_dir: &Path) -> Result<AcceptedMints> {
|
pub async fn load_accepted_mints(data_dir: &Path) -> Result<AcceptedMints> {
|
||||||
let path = data_dir.join(MINTS_FILE);
|
let network = load_network(data_dir).await;
|
||||||
|
let path = data_dir.join(network.mints_file());
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Ok(AcceptedMints {
|
return Ok(AcceptedMints {
|
||||||
mints: vec![default_mint_url()],
|
mints: vec![network.default_mint()],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let content = fs::read_to_string(&path)
|
let content = fs::read_to_string(&path)
|
||||||
.await
|
.await
|
||||||
.context("Failed to read accepted mints")?;
|
.context("Failed to read accepted mints")?;
|
||||||
let mints: AcceptedMints = serde_json::from_str(&content).unwrap_or(AcceptedMints {
|
let mints: AcceptedMints = serde_json::from_str(&content).unwrap_or(AcceptedMints {
|
||||||
mints: vec![default_mint_url()],
|
mints: vec![network.default_mint()],
|
||||||
});
|
});
|
||||||
Ok(mints)
|
Ok(mints)
|
||||||
}
|
}
|
||||||
@@ -272,7 +358,7 @@ pub async fn save_accepted_mints(data_dir: &Path, mints: &AcceptedMints) -> Resu
|
|||||||
fs::create_dir_all(&dir)
|
fs::create_dir_all(&dir)
|
||||||
.await
|
.await
|
||||||
.context("Failed to create wallet dir")?;
|
.context("Failed to create wallet dir")?;
|
||||||
let path = data_dir.join(MINTS_FILE);
|
let path = data_dir.join(load_network(data_dir).await.mints_file());
|
||||||
let content =
|
let content =
|
||||||
serde_json::to_string_pretty(mints).context("Failed to serialize accepted mints")?;
|
serde_json::to_string_pretty(mints).context("Failed to serialize accepted mints")?;
|
||||||
fs::write(&path, content)
|
fs::write(&path, content)
|
||||||
@@ -1885,4 +1971,73 @@ mod tests {
|
|||||||
other => panic!("expected swap into liquid target, got {:?}", other),
|
other => panic!("expected swap into liquid target, got {:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ecash_network_defaults_to_mainnet_and_leaves_files_alone() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let dir = tmp.path();
|
||||||
|
assert_eq!(load_network(dir).await, EcashNetwork::Mainnet);
|
||||||
|
// A node that never touches this setting has no new file.
|
||||||
|
assert!(!dir.join(NETWORK_FILE).exists());
|
||||||
|
assert_eq!(
|
||||||
|
load_wallet(dir).await.unwrap().mint_url,
|
||||||
|
default_mint_url(),
|
||||||
|
"mainnet must keep the original default mint"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn switching_to_testnet_uses_a_separate_purse_and_test_mint() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let dir = tmp.path();
|
||||||
|
|
||||||
|
// Put real coins in the mainnet wallet.
|
||||||
|
let mut real = load_wallet(dir).await.unwrap();
|
||||||
|
real.proofs.push(StoredProof {
|
||||||
|
proof: Proof {
|
||||||
|
amount: 1000,
|
||||||
|
id: "009a1f293253e41e".into(),
|
||||||
|
secret: "real".into(),
|
||||||
|
c: "02".into(),
|
||||||
|
},
|
||||||
|
mint_url: default_mint_url(),
|
||||||
|
spent: false,
|
||||||
|
reserved: false,
|
||||||
|
created_at: "2026-01-01T00:00:00Z".into(),
|
||||||
|
});
|
||||||
|
save_wallet(dir, &real).await.unwrap();
|
||||||
|
assert_eq!(load_wallet(dir).await.unwrap().balance(), 1000);
|
||||||
|
|
||||||
|
// Switching to testnet must show an EMPTY purse pointed at the test
|
||||||
|
// mint — never the real coins.
|
||||||
|
save_network(dir, EcashNetwork::Testnet).await.unwrap();
|
||||||
|
let test_wallet = load_wallet(dir).await.unwrap();
|
||||||
|
assert_eq!(test_wallet.balance(), 0, "test wallet must not see real coins");
|
||||||
|
assert!(test_wallet.mint_url.contains("testnut"));
|
||||||
|
assert!(load_accepted_mints(dir).await.unwrap().mints[0].contains("testnut"));
|
||||||
|
|
||||||
|
// Test coins are written to their own file...
|
||||||
|
let mut t = test_wallet;
|
||||||
|
t.proofs.push(StoredProof {
|
||||||
|
proof: Proof {
|
||||||
|
amount: 7,
|
||||||
|
id: "009a1f293253e41e".into(),
|
||||||
|
secret: "test".into(),
|
||||||
|
c: "02".into(),
|
||||||
|
},
|
||||||
|
mint_url: EcashNetwork::Testnet.default_mint(),
|
||||||
|
spent: false,
|
||||||
|
reserved: false,
|
||||||
|
created_at: "2026-01-01T00:00:00Z".into(),
|
||||||
|
});
|
||||||
|
save_wallet(dir, &t).await.unwrap();
|
||||||
|
assert!(dir.join("wallet/ecash.testnet.json").exists());
|
||||||
|
|
||||||
|
// ...and switching back finds the real balance exactly as it was.
|
||||||
|
save_network(dir, EcashNetwork::Mainnet).await.unwrap();
|
||||||
|
let back = load_wallet(dir).await.unwrap();
|
||||||
|
assert_eq!(back.balance(), 1000, "real funds must survive a round trip");
|
||||||
|
assert_eq!(back.proofs.len(), 1);
|
||||||
|
assert_eq!(back.proofs[0].proof.secret, "real");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -530,7 +530,9 @@ impl MintClient {
|
|||||||
return proofs.to_vec();
|
return proofs.to_vec();
|
||||||
}
|
}
|
||||||
|
|
||||||
let known = match self.get_keysets().await {
|
// The mint's own keyset list, in the reference implementation's shape
|
||||||
|
// so its NUT-02 resolver can consume it directly.
|
||||||
|
let known = match self.get_cdk_keysets().await {
|
||||||
Ok(k) => k,
|
Ok(k) => k,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("Could not list keysets to repair truncated keyset ids: {e:#}");
|
debug!("Could not list keysets to repair truncated keyset ids: {e:#}");
|
||||||
@@ -542,28 +544,42 @@ impl MintClient {
|
|||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|mut p| {
|
.map(|mut p| {
|
||||||
if !is_truncated_v2_keyset_id(&p.id) {
|
if let Some(full) = super::cashu::resolve_keyset_id(&p.id, &known) {
|
||||||
return p;
|
debug!("Expanded short keyset id {} to {} for swap", p.id, full);
|
||||||
}
|
p.id = full;
|
||||||
// Prefer an active keyset when a prefix somehow matches more
|
|
||||||
// than one; ambiguity beyond that is left to the mint.
|
|
||||||
let mut matches = known
|
|
||||||
.iter()
|
|
||||||
.filter(|k| k.id.len() == 66 && k.id.starts_with(&p.id))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
matches.sort_by_key(|k| !k.active);
|
|
||||||
if let Some(full) = matches.first() {
|
|
||||||
debug!(
|
|
||||||
"Expanded truncated v2 keyset id {} to {} for swap",
|
|
||||||
p.id, full.id
|
|
||||||
);
|
|
||||||
p.id = full.id.clone();
|
|
||||||
}
|
}
|
||||||
p
|
p
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The mint's keysets as upstream `KeySetInfo`, for NUT-02 id resolution.
|
||||||
|
async fn get_cdk_keysets(&self) -> Result<Vec<cashu::nuts::nut02::KeySetInfo>> {
|
||||||
|
let url = format!("{}/v1/keysets", self.url);
|
||||||
|
let res = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Failed to fetch mint keysets")?;
|
||||||
|
if !res.status().is_success() {
|
||||||
|
anyhow::bail!("Mint keysets request failed: {}", res.status());
|
||||||
|
}
|
||||||
|
let body: serde_json::Value = res.json().await.context("Failed to parse mint keysets")?;
|
||||||
|
// Deserialize per-entry and keep what parses: a mint may advertise a
|
||||||
|
// keyset in a unit or format this build doesn't model, and one such
|
||||||
|
// entry must not block resolving the id we actually need.
|
||||||
|
let list = body
|
||||||
|
.get("keysets")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(list
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|v| serde_json::from_value::<cashu::nuts::nut02::KeySetInfo>(v).ok())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn receive_token(&self, token: &CashuToken) -> Result<Vec<Proof>> {
|
pub async fn receive_token(&self, token: &CashuToken) -> Result<Vec<Proof>> {
|
||||||
let mut all_new_proofs = Vec::new();
|
let mut all_new_proofs = Vec::new();
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,35 @@
|
|||||||
|
|
||||||
<!-- ===================== Cashu Mints ===================== -->
|
<!-- ===================== Cashu Mints ===================== -->
|
||||||
<div v-show="activeTab === 'cashu'">
|
<div v-show="activeTab === 'cashu'">
|
||||||
|
<!-- Real vs test ecash. Each network keeps its own wallet, so switching
|
||||||
|
parks one balance and reveals the other — it never merges them. -->
|
||||||
|
<div class="mb-4 rounded-lg border border-white/10 bg-white/[0.04] p-3">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-white/80 text-sm font-medium">{{ ecashIsTest ? 'Test ecash' : 'Real ecash' }}</p>
|
||||||
|
<p class="text-white/40 text-xs mt-0.5">
|
||||||
|
{{ ecashIsTest
|
||||||
|
? 'Practice coins with no value, from a public test mint.'
|
||||||
|
: 'Real, spendable sats.' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer shrink-0">
|
||||||
|
<span class="text-white/60 text-xs">Test mode</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="ecashIsTest"
|
||||||
|
:disabled="switchingNetwork"
|
||||||
|
class="h-4 w-4 accent-orange-500"
|
||||||
|
@change="setEcashNetwork(($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p v-if="ecashIsTest" class="mt-2 text-xs text-orange-200">
|
||||||
|
Your real ecash balance is safe and untouched — it reappears when you turn test mode off.
|
||||||
|
</p>
|
||||||
|
<p v-if="networkError" class="mt-2 text-xs text-red-300">{{ networkError }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="text-white/60 text-sm mb-4">
|
<p class="text-white/60 text-sm mb-4">
|
||||||
Cashu ecash tokens can only be received from mints in this list. Add a mint's URL to accept tokens issued by it.
|
Cashu ecash tokens can only be received from mints in this list. Add a mint's URL to accept tokens issued by it.
|
||||||
</p>
|
</p>
|
||||||
@@ -359,12 +388,48 @@ watch(
|
|||||||
(open) => {
|
(open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
loadMints()
|
loadMints()
|
||||||
|
loadEcashNetwork()
|
||||||
if (fedimintBackendReady) loadFederations()
|
if (fedimintBackendReady) loadFederations()
|
||||||
loadArk()
|
loadArk()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ---- Ecash network (real vs test coins) ----
|
||||||
|
const ecashIsTest = ref(false)
|
||||||
|
const switchingNetwork = ref(false)
|
||||||
|
const networkError = ref('')
|
||||||
|
|
||||||
|
async function loadEcashNetwork() {
|
||||||
|
try {
|
||||||
|
const res = await rpcClient.call<{ is_test: boolean }>({ method: 'wallet.ecash-network' })
|
||||||
|
ecashIsTest.value = !!res.is_test
|
||||||
|
} catch {
|
||||||
|
// Older node without the setting: treat as real ecash, which is the
|
||||||
|
// safe reading — never imply a real balance is "test".
|
||||||
|
ecashIsTest.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setEcashNetwork(test: boolean) {
|
||||||
|
switchingNetwork.value = true
|
||||||
|
networkError.value = ''
|
||||||
|
try {
|
||||||
|
await rpcClient.call({
|
||||||
|
method: 'wallet.ecash-set-network',
|
||||||
|
params: { network: test ? 'testnet' : 'mainnet' },
|
||||||
|
})
|
||||||
|
ecashIsTest.value = test
|
||||||
|
// The mint list is per-network, so it must be re-read after a switch.
|
||||||
|
await loadMints()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
networkError.value = err instanceof Error ? err.message : 'Failed to switch network'
|
||||||
|
await loadEcashNetwork()
|
||||||
|
} finally {
|
||||||
|
switchingNetwork.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadMints() {
|
async function loadMints() {
|
||||||
loadingMints.value = true
|
loadingMints.value = true
|
||||||
mintError.value = ''
|
mintError.value = ''
|
||||||
|
|||||||
Executable
+183
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Headless exercise of every Cashu route the node exposes.
|
||||||
|
#
|
||||||
|
# Runs against the node's RPC exactly as the UI does, so it covers the real
|
||||||
|
# path: dispatcher -> handler -> wallet -> mint HTTP. Intended to be run on
|
||||||
|
# the node itself.
|
||||||
|
#
|
||||||
|
# ./scripts/test-ecash-routes.sh # uses testnet (safe)
|
||||||
|
# ECASH_TEST_NETWORK=mainnet ./scripts/... # REAL COINS, opt-in only
|
||||||
|
#
|
||||||
|
# Requires: the node's web password.
|
||||||
|
# ARCHY_PASSWORD='...' ./scripts/test-ecash-routes.sh
|
||||||
|
#
|
||||||
|
# Exit code is the number of failed checks, so CI can gate on it.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HOST="${ARCHY_HOST:-127.0.0.1}"
|
||||||
|
SCHEME="${ARCHY_SCHEME:-http}"
|
||||||
|
BASE="$SCHEME://$HOST"
|
||||||
|
NETWORK="${ECASH_TEST_NETWORK:-testnet}"
|
||||||
|
JAR="$(mktemp -t ecash-routes-XXXXXX.jar)"
|
||||||
|
trap 'rm -f "$JAR"' EXIT
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
ORIGINAL_NETWORK=""
|
||||||
|
|
||||||
|
log() { printf '%s\n' "$*"; }
|
||||||
|
ok() { PASS=$((PASS+1)); printf ' \033[32mPASS\033[0m %s\n' "$*"; }
|
||||||
|
bad() { FAIL=$((FAIL+1)); printf ' \033[31mFAIL\033[0m %s\n' "$*"; }
|
||||||
|
|
||||||
|
# rpc <method> [json-params] -> prints the full JSON-RPC response
|
||||||
|
rpc() {
|
||||||
|
local method="$1" params="${2:-}" body csrf
|
||||||
|
csrf="$(awk '/csrf_token/{print $NF}' "$JAR" 2>/dev/null | tail -1)"
|
||||||
|
if [ -n "$params" ]; then
|
||||||
|
body="{\"method\":\"$method\",\"params\":$params}"
|
||||||
|
else
|
||||||
|
body="{\"method\":\"$method\"}"
|
||||||
|
fi
|
||||||
|
curl -s --max-time 90 -b "$JAR" -H 'Content-Type: application/json' \
|
||||||
|
${csrf:+-H "X-CSRF-Token: $csrf"} \
|
||||||
|
-X POST "$BASE/rpc/v1" -d "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
jqf() { python3 -c "
|
||||||
|
import json,sys
|
||||||
|
try: d=json.load(sys.stdin)
|
||||||
|
except Exception: print(''); sys.exit()
|
||||||
|
cur=d
|
||||||
|
for k in sys.argv[1].split('.'):
|
||||||
|
if isinstance(cur,dict): cur=cur.get(k)
|
||||||
|
else: cur=None
|
||||||
|
print('' if cur is None else cur)" "$1"; }
|
||||||
|
|
||||||
|
err_of() { jqf error.message; }
|
||||||
|
|
||||||
|
login() {
|
||||||
|
local pw="${ARCHY_PASSWORD:-}"
|
||||||
|
if [ -z "$pw" ]; then
|
||||||
|
log "ARCHY_PASSWORD is not set — cannot authenticate."; exit 2
|
||||||
|
fi
|
||||||
|
curl -s -c "$JAR" --max-time 30 -H 'Content-Type: application/json' \
|
||||||
|
-X POST "$BASE/rpc/v1" \
|
||||||
|
-d "{\"method\":\"auth.login\",\"params\":{\"password\":\"$pw\"}}" >/dev/null
|
||||||
|
if ! grep -q session "$JAR" 2>/dev/null; then
|
||||||
|
log "Login failed — check ARCHY_PASSWORD."; exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log "== ecash route check =="
|
||||||
|
log "node: $BASE"
|
||||||
|
log "network: $NETWORK"
|
||||||
|
login
|
||||||
|
|
||||||
|
# ── network routes ────────────────────────────────────────────────────────
|
||||||
|
res="$(rpc wallet.ecash-network)"
|
||||||
|
ORIGINAL_NETWORK="$(printf '%s' "$res" | jqf result.network)"
|
||||||
|
[ -n "$ORIGINAL_NETWORK" ] && ok "ecash-network reports '$ORIGINAL_NETWORK'" \
|
||||||
|
|| bad "ecash-network: $(printf '%s' "$res" | err_of)"
|
||||||
|
|
||||||
|
# Always restore whatever the node was on, even on failure.
|
||||||
|
restore_network() {
|
||||||
|
if [ -n "$ORIGINAL_NETWORK" ]; then
|
||||||
|
rpc wallet.ecash-set-network "{\"network\":\"$ORIGINAL_NETWORK\"}" >/dev/null
|
||||||
|
log "restored network to '$ORIGINAL_NETWORK'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap 'restore_network; rm -f "$JAR"' EXIT
|
||||||
|
|
||||||
|
res="$(rpc wallet.ecash-set-network "{\"network\":\"$NETWORK\"}")"
|
||||||
|
[ "$(printf '%s' "$res" | jqf result.network)" = "$NETWORK" ] \
|
||||||
|
&& ok "switched to $NETWORK" || bad "set-network: $(printf '%s' "$res" | err_of)"
|
||||||
|
|
||||||
|
res="$(rpc wallet.ecash-set-network '{"network":"bogus"}')"
|
||||||
|
[ -n "$(printf '%s' "$res" | err_of)" ] \
|
||||||
|
&& ok "unknown network rejected" || bad "unknown network was accepted"
|
||||||
|
|
||||||
|
MINT="$(rpc wallet.ecash-network | jqf result.mint_url)"
|
||||||
|
log "mint: $MINT"
|
||||||
|
|
||||||
|
# ── read routes ───────────────────────────────────────────────────────────
|
||||||
|
res="$(rpc wallet.ecash-balance)"
|
||||||
|
START_BAL="$(printf '%s' "$res" | jqf result.cashu_sats)"
|
||||||
|
[ -n "$START_BAL" ] && ok "ecash-balance = ${START_BAL} sats" \
|
||||||
|
|| bad "ecash-balance: $(printf '%s' "$res" | err_of)"
|
||||||
|
|
||||||
|
res="$(rpc wallet.ecash-history)"
|
||||||
|
[ -z "$(printf '%s' "$res" | err_of)" ] && ok "ecash-history" \
|
||||||
|
|| bad "ecash-history: $(printf '%s' "$res" | err_of)"
|
||||||
|
|
||||||
|
# ── mint reachability (the route every other one depends on) ──────────────
|
||||||
|
if curl -s --max-time 20 "$MINT/v1/keysets" | grep -q keysets; then
|
||||||
|
ok "mint keysets reachable"
|
||||||
|
else
|
||||||
|
bad "mint $MINT unreachable — remaining checks will fail"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── mint quote (invoice issuance) ─────────────────────────────────────────
|
||||||
|
res="$(rpc wallet.ecash-mint '{"amount_sats":16}')"
|
||||||
|
QUOTE="$(printf '%s' "$res" | jqf result.quote_id)"
|
||||||
|
[ -n "$QUOTE" ] && ok "ecash-mint issued quote ${QUOTE:0:12}…" \
|
||||||
|
|| bad "ecash-mint: $(printf '%s' "$res" | err_of)"
|
||||||
|
|
||||||
|
# The test mint pays its own quotes, so the claim can be attempted directly.
|
||||||
|
# On mainnet this is expected to stay unpaid — that is not a failure.
|
||||||
|
if [ -n "$QUOTE" ]; then
|
||||||
|
res="$(rpc wallet.ecash-mint-claim "{\"quote_id\":\"$QUOTE\",\"amount_sats\":16}")"
|
||||||
|
claimed="$(printf '%s' "$res" | jqf result.amount_sats)"
|
||||||
|
if [ -n "$claimed" ]; then
|
||||||
|
ok "ecash-mint-claim minted ${claimed} sats"
|
||||||
|
elif [ "$NETWORK" = mainnet ]; then
|
||||||
|
ok "ecash-mint-claim correctly unpaid on mainnet"
|
||||||
|
else
|
||||||
|
bad "ecash-mint-claim: $(printf '%s' "$res" | err_of)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
BAL="$(rpc wallet.ecash-balance | jqf result.cashu_sats)"
|
||||||
|
log "balance after mint: ${BAL:-?} sats"
|
||||||
|
|
||||||
|
# ── send + receive round trip (the route that broke) ──────────────────────
|
||||||
|
if [ "${BAL:-0}" -ge 4 ] 2>/dev/null; then
|
||||||
|
res="$(rpc wallet.ecash-send '{"amount_sats":4}')"
|
||||||
|
TOKEN="$(printf '%s' "$res" | jqf result.token)"
|
||||||
|
if [ -n "$TOKEN" ]; then
|
||||||
|
ok "ecash-send produced a token (${#TOKEN} chars, ${TOKEN:0:7}…)"
|
||||||
|
|
||||||
|
res="$(rpc wallet.ecash-receive "{\"token\":\"$TOKEN\"}")"
|
||||||
|
got="$(printf '%s' "$res" | jqf result.received_sats)"
|
||||||
|
[ -n "$got" ] && ok "ecash-receive redeemed ${got} sats" \
|
||||||
|
|| bad "ecash-receive: $(printf '%s' "$res" | err_of)"
|
||||||
|
|
||||||
|
# Double-spend must be refused, not silently accepted.
|
||||||
|
res="$(rpc wallet.ecash-receive "{\"token\":\"$TOKEN\"}")"
|
||||||
|
[ -n "$(printf '%s' "$res" | err_of)" ] \
|
||||||
|
&& ok "double-redeem refused" || bad "double-redeem was ACCEPTED"
|
||||||
|
else
|
||||||
|
bad "ecash-send: $(printf '%s' "$res" | err_of)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log " (skipping send/receive — balance ${BAL:-0} sats too low)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── malformed input handling ──────────────────────────────────────────────
|
||||||
|
res="$(rpc wallet.ecash-receive '{"token":"not-a-token"}')"
|
||||||
|
[ -n "$(printf '%s' "$res" | err_of)" ] \
|
||||||
|
&& ok "garbage token rejected" || bad "garbage token was accepted"
|
||||||
|
|
||||||
|
# ── melt quote (spend to Lightning) ───────────────────────────────────────
|
||||||
|
if [ -n "${ECASH_TEST_BOLT11:-}" ]; then
|
||||||
|
res="$(rpc wallet.ecash-melt "{\"bolt11\":\"$ECASH_TEST_BOLT11\"}")"
|
||||||
|
mq="$(printf '%s' "$res" | jqf result.quote_id)"
|
||||||
|
[ -n "$mq" ] && ok "ecash-melt quoted ${mq:0:12}…" \
|
||||||
|
|| bad "ecash-melt: $(printf '%s' "$res" | err_of)"
|
||||||
|
else
|
||||||
|
log " (skipping melt — set ECASH_TEST_BOLT11 to a payable invoice)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "== $PASS passed, $FAIL failed =="
|
||||||
|
exit "$FAIL"
|
||||||
Reference in New Issue
Block a user