diff --git a/Cargo.lock b/Cargo.lock index 80818d6d7..f886fd964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arboard" @@ -1716,8 +1716,8 @@ dependencies = [ [[package]] name = "dapi-grpc" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "dash-platform-macros", "futures-core", @@ -1784,8 +1784,8 @@ dependencies = [ [[package]] name = "dash-context-provider" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "dpp", "drive", @@ -1835,7 +1835,7 @@ dependencies = [ "raw-cpuid", "rayon", "regex", - "reqwest 0.13.1", + "reqwest 0.13.2", "resvg", "rfd", "rusqlite", @@ -1850,6 +1850,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", + "tz-rs", "which 8.0.0", "winres", "zeroize", @@ -1860,8 +1861,8 @@ dependencies = [ [[package]] name = "dash-network" -version = "0.41.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53d699c9b551ac7d3644e11ca46dc3819277ff87#53d699c9b551ac7d3644e11ca46dc3819277ff87" +version = "0.42.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=12ba186fd24a85fcb4736f0868b393f8a5b58c46#12ba186fd24a85fcb4736f0868b393f8a5b58c46" dependencies = [ "bincode 2.0.1", "bincode_derive", @@ -1871,8 +1872,8 @@ dependencies = [ [[package]] name = "dash-platform-macros" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "heck", "quote", @@ -1881,8 +1882,8 @@ dependencies = [ [[package]] name = "dash-sdk" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "arc-swap", "async-trait", @@ -1915,8 +1916,8 @@ dependencies = [ [[package]] name = "dash-spv" -version = "0.41.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53d699c9b551ac7d3644e11ca46dc3819277ff87#53d699c9b551ac7d3644e11ca46dc3819277ff87" +version = "0.42.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=12ba186fd24a85fcb4736f0868b393f8a5b58c46#12ba186fd24a85fcb4736f0868b393f8a5b58c46" dependencies = [ "anyhow", "async-trait", @@ -1926,6 +1927,7 @@ dependencies = [ "clap", "dashcore", "dashcore_hashes", + "futures", "hex", "hickory-resolver", "indexmap 2.13.0", @@ -1938,6 +1940,7 @@ dependencies = [ "serde_json", "thiserror 1.0.69", "tokio", + "tokio-stream", "tokio-util", "tracing", "tracing-appender", @@ -1946,8 +1949,8 @@ dependencies = [ [[package]] name = "dashcore" -version = "0.41.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53d699c9b551ac7d3644e11ca46dc3819277ff87#53d699c9b551ac7d3644e11ca46dc3819277ff87" +version = "0.42.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=12ba186fd24a85fcb4736f0868b393f8a5b58c46#12ba186fd24a85fcb4736f0868b393f8a5b58c46" dependencies = [ "anyhow", "base64-compat", @@ -1972,13 +1975,13 @@ dependencies = [ [[package]] name = "dashcore-private" -version = "0.41.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53d699c9b551ac7d3644e11ca46dc3819277ff87#53d699c9b551ac7d3644e11ca46dc3819277ff87" +version = "0.42.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=12ba186fd24a85fcb4736f0868b393f8a5b58c46#12ba186fd24a85fcb4736f0868b393f8a5b58c46" [[package]] name = "dashcore-rpc" -version = "0.41.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53d699c9b551ac7d3644e11ca46dc3819277ff87#53d699c9b551ac7d3644e11ca46dc3819277ff87" +version = "0.42.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=12ba186fd24a85fcb4736f0868b393f8a5b58c46#12ba186fd24a85fcb4736f0868b393f8a5b58c46" dependencies = [ "dashcore-rpc-json", "hex", @@ -1990,8 +1993,8 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" -version = "0.41.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53d699c9b551ac7d3644e11ca46dc3819277ff87#53d699c9b551ac7d3644e11ca46dc3819277ff87" +version = "0.42.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=12ba186fd24a85fcb4736f0868b393f8a5b58c46#12ba186fd24a85fcb4736f0868b393f8a5b58c46" dependencies = [ "bincode 2.0.1", "dashcore", @@ -2005,8 +2008,8 @@ dependencies = [ [[package]] name = "dashcore_hashes" -version = "0.41.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53d699c9b551ac7d3644e11ca46dc3819277ff87#53d699c9b551ac7d3644e11ca46dc3819277ff87" +version = "0.42.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=12ba186fd24a85fcb4736f0868b393f8a5b58c46#12ba186fd24a85fcb4736f0868b393f8a5b58c46" dependencies = [ "bincode 2.0.1", "dashcore-private", @@ -2030,8 +2033,8 @@ dependencies = [ [[package]] name = "dashpay-contract" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "platform-value", "platform-version", @@ -2041,8 +2044,8 @@ dependencies = [ [[package]] name = "data-contracts" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "dashpay-contract", "dpns-contract", @@ -2294,8 +2297,8 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "dpns-contract" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "platform-value", "platform-version", @@ -2305,8 +2308,8 @@ dependencies = [ [[package]] name = "dpp" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "anyhow", "async-trait", @@ -2353,8 +2356,8 @@ dependencies = [ [[package]] name = "drive" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "bincode 2.0.1", "byteorder", @@ -2378,8 +2381,8 @@ dependencies = [ [[package]] name = "drive-proof-verifier" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "bincode 2.0.1", "dapi-grpc", @@ -2960,8 +2963,8 @@ dependencies = [ [[package]] name = "feature-flags-contract" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "platform-value", "platform-version", @@ -3286,6 +3289,19 @@ dependencies = [ "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 = "ghash" version = "0.5.1" @@ -4090,6 +4106,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -4266,9 +4288,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" dependencies = [ "jiff-static", "log", @@ -4279,9 +4301,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" dependencies = [ "proc-macro2", "quote", @@ -4362,8 +4384,8 @@ dependencies = [ [[package]] name = "key-wallet" -version = "0.41.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53d699c9b551ac7d3644e11ca46dc3819277ff87#53d699c9b551ac7d3644e11ca46dc3819277ff87" +version = "0.42.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=12ba186fd24a85fcb4736f0868b393f8a5b58c46#12ba186fd24a85fcb4736f0868b393f8a5b58c46" dependencies = [ "async-trait", "base58ck", @@ -4389,22 +4411,24 @@ dependencies = [ [[package]] name = "key-wallet-manager" -version = "0.41.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53d699c9b551ac7d3644e11ca46dc3819277ff87#53d699c9b551ac7d3644e11ca46dc3819277ff87" +version = "0.42.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=12ba186fd24a85fcb4736f0868b393f8a5b58c46#12ba186fd24a85fcb4736f0868b393f8a5b58c46" dependencies = [ "async-trait", "bincode 2.0.1", "dashcore", "dashcore_hashes", "key-wallet", + "rayon", "secp256k1", + "tokio", "zeroize", ] [[package]] name = "keyword-search-contract" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "platform-value", "platform-version", @@ -4472,6 +4496,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lhash" version = "1.1.0" @@ -4589,8 +4619,8 @@ dependencies = [ [[package]] name = "masternode-reward-shares-contract" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "platform-value", "platform-version", @@ -4609,9 +4639,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" @@ -5419,9 +5449,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.6.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" dependencies = [ "num-traits", ] @@ -5653,8 +5683,8 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "platform-serialization" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "bincode 2.0.1", "platform-version", @@ -5662,8 +5692,8 @@ dependencies = [ [[package]] name = "platform-serialization-derive" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "proc-macro2", "quote", @@ -5673,8 +5703,8 @@ dependencies = [ [[package]] name = "platform-value" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "base64 0.22.1", "bincode 2.0.1", @@ -5693,19 +5723,20 @@ dependencies = [ [[package]] name = "platform-version" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "bincode 2.0.1", "grovedb-version", + "once_cell", "thiserror 2.0.18", "versioned-feature-core 1.0.0 (git+https://github.com/dashpay/versioned-feature-core)", ] [[package]] name = "platform-versioning" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "proc-macro2", "quote", @@ -6269,9 +6300,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", @@ -6306,7 +6337,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] @@ -6413,8 +6444,8 @@ dependencies = [ [[package]] name = "rs-dapi-client" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "backon", "chrono", @@ -6657,9 +6688,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -7343,12 +7374,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix 1.1.3", "windows-sys 0.61.2", @@ -7484,9 +7515,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -7505,9 +7536,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -7566,8 +7597,8 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "token-history-contract" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "platform-value", "platform-version", @@ -7632,6 +7663,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -7823,7 +7855,7 @@ dependencies = [ "tower-service", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", "web-sys", ] @@ -7992,6 +8024,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "tz-rs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc6c929ffa10fb34f4a3c7e9a73620a83ef2e85e47f9ec3381b8289e6762f42" + [[package]] name = "uds_windows" version = "1.1.0" @@ -8038,9 +8076,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-normalization" @@ -8117,9 +8155,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "ureq" -version = "3.1.4" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" dependencies = [ "base64 0.22.1", "flate2", @@ -8307,8 +8345,8 @@ dependencies = [ [[package]] name = "wallet-utils-contract" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "platform-value", "platform-version", @@ -8340,6 +8378,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +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 = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -8399,6 +8446,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -8412,6 +8481,31 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.12" @@ -8569,9 +8663,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" dependencies = [ "core-foundation 0.10.1", "jni", @@ -9573,11 +9667,93 @@ 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 2.13.0", + "prettyplease", + "syn 2.0.114", + "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 2.0.114", + "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 2.10.0", + "indexmap 2.13.0", + "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 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "withdrawals-contract" -version = "3.0.1" -source = "git+https://github.com/dashpay/platform?rev=806322247caf1037c6d73d3620804e27a2170a3d#806322247caf1037c6d73d3620804e27a2170a3d" +version = "3.0.0" +source = "git+https://github.com/dashpay/platform?rev=060515987a#060515987a9d54bb3046bc8deca821f449649856" dependencies = [ "num_enum 0.5.11", "platform-value", @@ -9794,18 +9970,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.38" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.38" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -9926,9 +10102,9 @@ dependencies = [ [[package]] name = "zip" -version = "7.3.0" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "268bf6f9ceb991e07155234071501490bb41fd1e39c6a588106dad10ae2a5804" +checksum = "cc12baa6db2b15a140161ce53d72209dacea594230798c24774139b54ecaa980" dependencies = [ "crc32fast", "flate2", @@ -9946,9 +10122,9 @@ checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" [[package]] name = "zmq" diff --git a/Cargo.toml b/Cargo.toml index d32127c97..9a5279417 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ qrcode = "0.14.1" nix = { version = "0.31.1", features = ["signal"] } eframe = { version = "0.33.3", features = ["persistence"] } base64 = "0.22.1" -dash-sdk = { git = "https://github.com/dashpay/platform", rev = "806322247caf1037c6d73d3620804e27a2170a3d", features = [ +dash-sdk = { git = "https://github.com/dashpay/platform", rev = "060515987a", features = [ "core_key_wallet", "core_key_wallet_manager", "core_bincode", diff --git a/src/backend_task/core/create_asset_lock.rs b/src/backend_task/core/create_asset_lock.rs index 94faf5379..5111d83dd 100644 --- a/src/backend_task/core/create_asset_lock.rs +++ b/src/backend_task/core/create_asset_lock.rs @@ -1,13 +1,12 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::wallet::Wallet; -use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dpp::balances::credits::CREDITS_PER_DUFF; use dash_sdk::dpp::fee::Credits; use std::sync::{Arc, RwLock}; impl AppContext { - pub fn create_registration_asset_lock( + pub async fn create_registration_asset_lock( &self, wallet: Arc>, amount: Credits, @@ -39,10 +38,8 @@ impl AppContext { } // Broadcast the transaction - self.core_client - .read() - .expect("Core client lock was poisoned") - .send_raw_transaction(&asset_lock_transaction) + self.broadcast_raw_transaction(&asset_lock_transaction) + .await .map_err(|e| format!("Failed to broadcast asset lock transaction: {}", e))?; // Update wallet UTXOs @@ -59,6 +56,8 @@ impl AppContext { .drop_utxo(utxo, &self.network.to_string()) .map_err(|e| e.to_string())?; } + + wallet_guard.recalculate_affected_address_balances(&used_utxos, self)?; } Ok(BackendTaskSuccessResult::Message(format!( @@ -67,7 +66,7 @@ impl AppContext { ))) } - pub fn create_top_up_asset_lock( + pub async fn create_top_up_asset_lock( &self, wallet: Arc>, amount: Credits, @@ -101,10 +100,8 @@ impl AppContext { } // Broadcast the transaction - self.core_client - .read() - .expect("Core client lock was poisoned") - .send_raw_transaction(&asset_lock_transaction) + self.broadcast_raw_transaction(&asset_lock_transaction) + .await .map_err(|e| format!("Failed to broadcast asset lock transaction: {}", e))?; // Update wallet UTXOs @@ -121,6 +118,8 @@ impl AppContext { .drop_utxo(utxo, &self.network.to_string()) .map_err(|e| e.to_string())?; } + + wallet_guard.recalculate_affected_address_balances(&used_utxos, self)?; } Ok(BackendTaskSuccessResult::Message(format!( diff --git a/src/backend_task/core/mod.rs b/src/backend_task/core/mod.rs index bdad43d1f..543a1614a 100644 --- a/src/backend_task/core/mod.rs +++ b/src/backend_task/core/mod.rs @@ -255,9 +255,11 @@ impl AppContext { .map(|_| BackendTaskSuccessResult::None), CoreTask::CreateRegistrationAssetLock(wallet, amount, identity_index) => self .create_registration_asset_lock(wallet, amount, true, identity_index) + .await .map_err(|e| format!("Error creating asset lock: {}", e)), CoreTask::CreateTopUpAssetLock(wallet, amount, identity_index, top_up_index) => self .create_top_up_asset_lock(wallet, amount, true, identity_index, top_up_index) + .await .map_err(|e| format!("Error creating top up asset lock: {}", e)), CoreTask::SendWalletPayment { wallet, request } => { self.send_wallet_payment(wallet, request).await @@ -483,7 +485,12 @@ impl AppContext { const FALLBACK_STEP: u64 = 100; let network = self.wallet_network_key(); - let current_height = wm.current_height(); + let current_height = self + .spv_manager() + .status() + .sync_progress + .map(|p| p.header_height) + .ok_or("Cannot build transaction: SPV sync height is not yet known")?; let total_amount: u64 = recipients.iter().map(|(_, amt)| *amt).sum(); let mut scale_factor = 1.0f64; let mut attempted_fallback = false; diff --git a/src/backend_task/dashpay/incoming_payments.rs b/src/backend_task/dashpay/incoming_payments.rs index 5390fc112..5ddcbe95c 100644 --- a/src/backend_task/dashpay/incoming_payments.rs +++ b/src/backend_task/dashpay/incoming_payments.rs @@ -283,7 +283,7 @@ pub fn match_transaction_to_contact( } /// Process an incoming transaction that was detected by SPV -/// This should be called when SpvEvent::TransactionDetected is received +/// This should be called when WalletEvent::TransactionReceived is received pub async fn process_incoming_payment( app_context: &Arc, tx_id: &str, diff --git a/src/backend_task/identity/register_identity.rs b/src/backend_task/identity/register_identity.rs index 855f08387..47ae6fa5d 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -1,11 +1,11 @@ use crate::backend_task::identity::{IdentityRegistrationInfo, RegisterIdentityFundingMethod}; use crate::backend_task::{BackendTaskSuccessResult, FeeResult}; -use crate::context::{AppContext, get_transaction_info_via_dapi}; +use crate::context::{AppContext, get_transaction_info}; use crate::model::fee_estimation::PlatformFeeEstimator; use crate::model::proof_log_item::{ProofLogItem, RequestType}; use crate::model::qualified_identity::{IdentityStatus, IdentityType, QualifiedIdentity}; +use crate::spv::CoreBackendMode; use dash_sdk::dash_spv::Network; -use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dpp::ProtocolError; use dash_sdk::dpp::address_funds::PlatformAddress; use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo; @@ -22,7 +22,6 @@ use dash_sdk::platform::{Fetch, FetchMany, Identity}; use dash_sdk::query_types::AddressInfo; use dash_sdk::{Error, Sdk}; use std::collections::BTreeMap; -use std::time::Duration; impl AppContext { pub(super) async fn register_identity( @@ -65,7 +64,7 @@ impl AppContext { asset_lock_proof.as_ref() { // we need to make sure the instant send asset lock is recent - let tx_info = get_transaction_info_via_dapi(&sdk, &tx_id).await?; + let tx_info = get_transaction_info(&sdk, &tx_id).await?; if tx_info.is_chain_locked && tx_info.height > 0 && tx_info.confirmations > 8 { // Transaction is old enough that instant lock may have expired @@ -107,24 +106,34 @@ impl AppContext { Some(self), ) { Ok(transaction) => transaction, - Err(_) => { - wallet - .reload_utxos( - &self - .core_client - .read() - .expect("Core client lock was poisoned"), - self.network, - Some(self), - ) - .map_err(|e| e.to_string())?; - wallet.registration_asset_lock_transaction( - sdk.network, - amount, - true, - identity_index, - Some(self), - )? + Err(e) => { + match self.core_backend_mode() { + CoreBackendMode::Rpc => { + wallet + .reload_utxos( + &self + .core_client + .read() + .expect("Core client lock was poisoned"), + self.network, + Some(self), + ) + .map_err(|e| e.to_string())?; + wallet.registration_asset_lock_transaction( + sdk.network, + amount, + true, + identity_index, + Some(self), + )? + } + CoreBackendMode::Spv => { + // SPV wallet state is authoritative — UTXOs are synced + // continuously via compact block filters. No Core RPC + // fallback available. + return Err(e); + } + } } } }; @@ -136,11 +145,8 @@ impl AppContext { proofs.insert(tx_id, None); } - self.core_client - .read() - .expect("Core client lock was poisoned") - .send_raw_transaction(&asset_lock_transaction) - .map_err(|e| e.to_string())?; + self.broadcast_raw_transaction(&asset_lock_transaction) + .await?; // Store the asset lock transaction in the database immediately after sending. // This ensures it's tracked even if the proof times out or identity creation fails. @@ -172,47 +178,10 @@ impl AppContext { .map_err(|e| e.to_string())?; } - // Update address_balances for affected addresses - let affected_addresses: std::collections::BTreeSet<_> = - used_utxos.values().map(|(_, addr)| addr.clone()).collect(); - for address in affected_addresses { - // Recalculate balance from remaining UTXOs for this address - let new_balance = wallet - .utxos - .get(&address) - .map(|utxo_map| utxo_map.values().map(|tx_out| tx_out.value).sum()) - .unwrap_or(0); - let _ = wallet.update_address_balance(&address, new_balance, self); - } + wallet.recalculate_affected_address_balances(&used_utxos, self)?; } - // Wait for asset lock proof with timeout (2 minutes) - const ASSET_LOCK_PROOF_TIMEOUT: Duration = Duration::from_secs(120); - let asset_lock_proof = match tokio::time::timeout(ASSET_LOCK_PROOF_TIMEOUT, async { - loop { - { - let proofs = self.transactions_waiting_for_finality.lock().unwrap(); - if let Some(Some(proof)) = proofs.get(&tx_id) { - return proof.clone(); - } - } - tokio::time::sleep(Duration::from_millis(200)).await; - } - }) - .await - { - Ok(proof) => proof, - Err(_) => { - // Clean up on timeout - let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); - proofs.remove(&tx_id); - return Err(format!( - "Timeout waiting for asset lock proof after {} seconds. \ - The transaction may not have been confirmed by the network.", - ASSET_LOCK_PROOF_TIMEOUT.as_secs() - )); - } - }; + let asset_lock_proof = self.wait_for_asset_lock_proof(tx_id).await?; (asset_lock_proof, asset_lock_proof_private_key, tx_id) } @@ -287,11 +256,8 @@ impl AppContext { proofs.insert(tx_id, None); } - self.core_client - .read() - .expect("Core client lock was poisoned") - .send_raw_transaction(&asset_lock_transaction) - .map_err(|e| e.to_string())?; + self.broadcast_raw_transaction(&asset_lock_transaction) + .await?; // Store the asset lock transaction in the database immediately after sending. // This ensures it's tracked even if the proof times out or identity creation fails. @@ -317,42 +283,10 @@ impl AppContext { .drop_utxo(&utxo, &self.network.to_string()) .map_err(|e| e.to_string())?; - // Update address_balance for the affected address - let new_balance = wallet - .utxos - .get(&input_address) - .map(|utxo_map| utxo_map.values().map(|tx_out| tx_out.value).sum()) - .unwrap_or(0); - let _ = wallet.update_address_balance(&input_address, new_balance, self); + wallet.recalculate_address_balance(&input_address, self)?; } - // Wait for asset lock proof with timeout (2 minutes) - const ASSET_LOCK_PROOF_TIMEOUT: Duration = Duration::from_secs(120); - let asset_lock_proof = match tokio::time::timeout(ASSET_LOCK_PROOF_TIMEOUT, async { - loop { - { - let proofs = self.transactions_waiting_for_finality.lock().unwrap(); - if let Some(Some(proof)) = proofs.get(&tx_id) { - return proof.clone(); - } - } - tokio::time::sleep(Duration::from_millis(200)).await; - } - }) - .await - { - Ok(proof) => proof, - Err(_) => { - // Clean up on timeout - let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); - proofs.remove(&tx_id); - return Err(format!( - "Timeout waiting for asset lock proof after {} seconds. \ - The transaction may not have been confirmed by the network.", - ASSET_LOCK_PROOF_TIMEOUT.as_secs() - )); - } - }; + let asset_lock_proof = self.wait_for_asset_lock_proof(tx_id).await?; (asset_lock_proof, asset_lock_proof_private_key, tx_id) } @@ -481,7 +415,7 @@ impl AppContext { || e.contains("wasn't created recently") { // Try to use chain asset lock proof instead - let tx_info = get_transaction_info_via_dapi(&sdk, &tx_id).await?; + let tx_info = get_transaction_info(&sdk, &tx_id).await?; if tx_info.is_chain_locked && tx_info.height > 0 { let tx_block_height = tx_info.height; diff --git a/src/backend_task/identity/top_up_identity.rs b/src/backend_task/identity/top_up_identity.rs index 5a309e89f..76d115a26 100644 --- a/src/backend_task/identity/top_up_identity.rs +++ b/src/backend_task/identity/top_up_identity.rs @@ -1,10 +1,10 @@ use crate::backend_task::identity::{IdentityTopUpInfo, TopUpIdentityFundingMethod}; use crate::backend_task::{BackendTaskSuccessResult, FeeResult}; -use crate::context::{AppContext, get_transaction_info_via_dapi}; +use crate::context::{AppContext, get_transaction_info}; use crate::model::fee_estimation::PlatformFeeEstimator; use crate::model::proof_log_item::{ProofLogItem, RequestType}; +use crate::spv::CoreBackendMode; use dash_sdk::Error; -use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dpp::ProtocolError; use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo; use dash_sdk::dpp::dashcore::OutPoint; @@ -16,7 +16,6 @@ use dash_sdk::dpp::state_transition::identity_topup_transition::IdentityTopUpTra use dash_sdk::dpp::state_transition::identity_topup_transition::methods::IdentityTopUpTransitionMethodsV0; use dash_sdk::platform::Fetch; use dash_sdk::platform::transition::top_up_identity::TopUpIdentity; -use std::time::Duration; impl AppContext { pub(super) async fn top_up_identity( @@ -59,7 +58,7 @@ impl AppContext { ) = asset_lock_proof.as_ref() { // we need to make sure the instant send asset lock is recent - let tx_info = get_transaction_info_via_dapi(&sdk, &tx_id).await?; + let tx_info = get_transaction_info(&sdk, &tx_id).await?; if tx_info.is_chain_locked && tx_info.height > 0 @@ -115,26 +114,27 @@ impl AppContext { Some(self), ) { Ok(transaction) => transaction, - Err(_) => { - wallet - .reload_utxos( - &self - .core_client - .read() - .expect("Core client lock was poisoned"), - self.network, + Err(e) => match self.core_backend_mode() { + CoreBackendMode::Rpc => { + let core_client = self.core_client.read().map_err(|e| { + format!("Core client lock was poisoned: {}", e) + })?; + wallet + .reload_utxos(&core_client, self.network, Some(self)) + .map_err(|e| e.to_string())?; + wallet.top_up_asset_lock_transaction( + sdk.network, + amount, + true, + identity_index, + top_up_index, Some(self), - ) - .map_err(|e| e.to_string())?; - wallet.top_up_asset_lock_transaction( - sdk.network, - amount, - true, - identity_index, - top_up_index, - Some(self), - )? - } + )? + } + CoreBackendMode::Spv => { + return Err(e); + } + }, }; ( tx_result.0, @@ -158,11 +158,8 @@ impl AppContext { proofs.insert(tx_id, None); } - self.core_client - .read() - .expect("Core client lock was poisoned") - .send_raw_transaction(&asset_lock_transaction) - .map_err(|e| e.to_string())?; + self.broadcast_raw_transaction(&asset_lock_transaction) + .await?; // Store the asset lock transaction in the database immediately after sending. // This ensures it's tracked even if the proof times out or top-up fails. @@ -189,50 +186,10 @@ impl AppContext { .map_err(|e| e.to_string())?; } - // Update address_balances for affected addresses - let affected_addresses: std::collections::BTreeSet<_> = - used_utxos.values().map(|(_, addr)| addr.clone()).collect(); - for address in affected_addresses { - // Recalculate balance from remaining UTXOs for this address - let new_balance = wallet - .utxos - .get(&address) - .map(|utxo_map| utxo_map.values().map(|tx_out| tx_out.value).sum()) - .unwrap_or(0); - let _ = wallet.update_address_balance(&address, new_balance, self); - } + wallet.recalculate_affected_address_balances(&used_utxos, self)?; } - // Wait for asset lock proof with timeout (2 minutes) - const ASSET_LOCK_PROOF_TIMEOUT: Duration = Duration::from_secs(120); - let asset_lock_proof = - match tokio::time::timeout(ASSET_LOCK_PROOF_TIMEOUT, async { - loop { - { - let proofs = - self.transactions_waiting_for_finality.lock().unwrap(); - if let Some(Some(proof)) = proofs.get(&tx_id) { - return proof.clone(); - } - } - tokio::time::sleep(Duration::from_millis(200)).await; - } - }) - .await - { - Ok(proof) => proof, - Err(_) => { - // Clean up on timeout - let mut proofs = - self.transactions_waiting_for_finality.lock().unwrap(); - proofs.remove(&tx_id); - return Err(format!( - "Timeout waiting for asset lock proof after {} seconds. \ - The transaction may not have been confirmed by the network.", - ASSET_LOCK_PROOF_TIMEOUT.as_secs() - )); - } - }; + let asset_lock_proof = self.wait_for_asset_lock_proof(tx_id).await?; ( asset_lock_proof, @@ -277,11 +234,8 @@ impl AppContext { proofs.insert(tx_id, None); } - self.core_client - .read() - .expect("Core client lock was poisoned") - .send_raw_transaction(&asset_lock_transaction) - .map_err(|e| e.to_string())?; + self.broadcast_raw_transaction(&asset_lock_transaction) + .await?; // Store the asset lock transaction in the database immediately after sending. // This ensures it's tracked even if the proof times out or top-up fails. @@ -306,45 +260,10 @@ impl AppContext { .drop_utxo(&utxo, &self.network.to_string()) .map_err(|e| e.to_string())?; - // Update address_balance for the affected address - let new_balance = wallet - .utxos - .get(&input_address) - .map(|utxo_map| utxo_map.values().map(|tx_out| tx_out.value).sum()) - .unwrap_or(0); - let _ = wallet.update_address_balance(&input_address, new_balance, self); + wallet.recalculate_address_balance(&input_address, self)?; } - // Wait for asset lock proof with timeout (2 minutes) - const ASSET_LOCK_PROOF_TIMEOUT: Duration = Duration::from_secs(120); - let asset_lock_proof = - match tokio::time::timeout(ASSET_LOCK_PROOF_TIMEOUT, async { - loop { - { - let proofs = - self.transactions_waiting_for_finality.lock().unwrap(); - if let Some(Some(proof)) = proofs.get(&tx_id) { - return proof.clone(); - } - } - tokio::time::sleep(Duration::from_millis(200)).await; - } - }) - .await - { - Ok(proof) => proof, - Err(_) => { - // Clean up on timeout - let mut proofs = - self.transactions_waiting_for_finality.lock().unwrap(); - proofs.remove(&tx_id); - return Err(format!( - "Timeout waiting for asset lock proof after {} seconds. \ - The transaction may not have been confirmed by the network.", - ASSET_LOCK_PROOF_TIMEOUT.as_secs() - )); - } - }; + let asset_lock_proof = self.wait_for_asset_lock_proof(tx_id).await?; ( asset_lock_proof, @@ -406,7 +325,7 @@ impl AppContext { || error_string.contains("wasn't created recently") { // Try to use chain asset lock proof instead - let tx_info = get_transaction_info_via_dapi(&sdk, &tx_id).await?; + let tx_info = get_transaction_info(&sdk, &tx_id).await?; if tx_info.is_chain_locked && tx_info.height > 0 { let tx_block_height = tx_info.height; diff --git a/src/backend_task/wallet/fund_platform_address_from_asset_lock.rs b/src/backend_task/wallet/fund_platform_address_from_asset_lock.rs index 5da5ff228..02983677c 100644 --- a/src/backend_task/wallet/fund_platform_address_from_asset_lock.rs +++ b/src/backend_task/wallet/fund_platform_address_from_asset_lock.rs @@ -46,7 +46,7 @@ impl AppContext { }; // Check if we need to convert an old instant lock proof to a chain lock proof - use crate::context::get_transaction_info_via_dapi; + use crate::context::get_transaction_info; use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo; use dash_sdk::platform::Fetch; @@ -57,7 +57,7 @@ impl AppContext { let tx_id = instant_asset_lock_proof.transaction().txid(); // Query DAPI to check if the transaction has been chain-locked - let tx_info = get_transaction_info_via_dapi(&sdk, &tx_id).await?; + let tx_info = get_transaction_info(&sdk, &tx_id).await?; if tx_info.is_chain_locked && tx_info.height > 0 && tx_info.confirmations > 8 { // Transaction has been chain-locked with sufficient confirmations diff --git a/src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs b/src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs index ac132b754..aa67973ab 100644 --- a/src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs +++ b/src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs @@ -124,18 +124,7 @@ impl AppContext { .map_err(|e| e.to_string())?; } - // Update address_balances for affected addresses - let affected_addresses: std::collections::BTreeSet<_> = - used_utxos.values().map(|(_, addr)| addr.clone()).collect(); - for address in affected_addresses { - // Recalculate balance from remaining UTXOs for this address - let new_balance = wallet - .utxos - .get(&address) - .map(|utxo_map| utxo_map.values().map(|tx_out| tx_out.value).sum()) - .unwrap_or(0); - let _ = wallet.update_address_balance(&address, new_balance, self); - } + wallet.recalculate_affected_address_balances(&used_utxos, self)?; } // Step 5: Wait for asset lock proof (InstantLock or ChainLock) with timeout diff --git a/src/context/mod.rs b/src/context/mod.rs index fd4e65120..5e321f40c 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -4,7 +4,7 @@ mod settings_db; mod transaction_processing; mod wallet_lifecycle; -pub(crate) use transaction_processing::get_transaction_info_via_dapi; +pub(crate) use transaction_processing::get_transaction_info; use crate::app_dir::core_cookie_path; use crate::components::core_zmq_listener::ZMQConnectionEvent; diff --git a/src/context/transaction_processing.rs b/src/context/transaction_processing.rs index 37fac24d2..79104db57 100644 --- a/src/context/transaction_processing.rs +++ b/src/context/transaction_processing.rs @@ -1,5 +1,7 @@ use super::AppContext; +use crate::spv::CoreBackendMode; use dash_sdk::Sdk; +use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::transaction::special_transaction::TransactionPayload::AssetLockPayloadType; use dash_sdk::dpp::dashcore::{Address, InstantLock, OutPoint, Transaction, TxOut, Txid}; @@ -10,6 +12,68 @@ use rusqlite::Result; use std::collections::HashMap; impl AppContext { + /// Broadcast a raw transaction via Core RPC or SPV depending on backend mode. + pub(crate) async fn broadcast_raw_transaction(&self, tx: &Transaction) -> Result { + match self.core_backend_mode() { + CoreBackendMode::Rpc => self + .core_client + .read() + .map_err(|e| format!("core client lock poisoned: {}", e))? + .send_raw_transaction(tx) + .map_err(|e| e.to_string()), + CoreBackendMode::Spv => { + self.spv_manager.broadcast_transaction(tx).await?; + Ok(tx.txid()) + } + } + } + + /// Wait for an asset lock proof (InstantLock or ChainLock) for the given transaction. + /// + /// Polls `transactions_waiting_for_finality` until a proof appears, with a + /// backend-mode-dependent timeout (SPV: 5 min, RPC: 2 min). Cleans up the + /// tracking entry on both success and timeout. + pub(crate) async fn wait_for_asset_lock_proof( + &self, + tx_id: Txid, + ) -> Result { + use tokio::time::Duration; + + let timeout_duration = match self.core_backend_mode() { + CoreBackendMode::Spv => Duration::from_secs(300), + CoreBackendMode::Rpc => Duration::from_secs(120), + }; + + match tokio::time::timeout(timeout_duration, async { + loop { + { + let proofs = self.transactions_waiting_for_finality.lock().unwrap(); + if let Some(Some(proof)) = proofs.get(&tx_id) { + return proof.clone(); + } + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + }) + .await + { + Ok(proof) => { + let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); + proofs.remove(&tx_id); + Ok(proof) + } + Err(_) => { + let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); + proofs.remove(&tx_id); + Err(format!( + "Timeout waiting for asset lock proof after {} seconds. \ + The transaction may not have been confirmed by the network.", + timeout_duration.as_secs() + )) + } + } + } + pub(crate) fn received_transaction_finality( &self, tx: &Transaction, @@ -205,7 +269,7 @@ pub(crate) struct DapiTransactionInfo { /// Query transaction info from DAPI. Works in both SPV and RPC modes /// since DAPI (platform gRPC) is always available via the SDK. -pub(crate) async fn get_transaction_info_via_dapi( +pub(crate) async fn get_transaction_info( sdk: &Sdk, tx_id: &Txid, ) -> Result { diff --git a/src/context/wallet_lifecycle.rs b/src/context/wallet_lifecycle.rs index cf66cc262..ceef9c658 100644 --- a/src/context/wallet_lifecycle.rs +++ b/src/context/wallet_lifecycle.rs @@ -1,9 +1,11 @@ use super::AppContext; +use super::get_transaction_info; use crate::model::wallet::{ - AddressInfo as WalletAddressInfo, DerivationPathReference, DerivationPathType, Wallet, - WalletSeedHash, + AddressInfo as WalletAddressInfo, DerivationPathHelpers, DerivationPathReference, + DerivationPathType, Wallet, WalletSeedHash, WalletTransaction, }; -use crate::spv::{CoreBackendMode, SpvManager}; +use crate::spv::{AssetLockFinalityEvent, CoreBackendMode, SpvManager}; +use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::{Address, Network}; use dash_sdk::dpp::key_wallet::Network as WalletNetwork; use dash_sdk::dpp::key_wallet::account::AccountType; @@ -42,8 +44,33 @@ impl AppContext { } pub fn start_spv(self: &Arc) -> Result<(), String> { - self.spv_manager.start()?; + // Skip if SPV is already active — avoids orphaned listener tasks from + // re-registering channels while existing handlers still hold old senders. + if self.spv_manager.status().status.is_active() { + return Ok(()); + } + + // Count wallets that will be loaded into SPV (open wallets with accessible seeds). + // This is read synchronously so the SPV thread can wait for exactly this many. + let expected_wallets = self + .wallets + .read() + .map(|guard| { + guard + .values() + .filter(|w| { + w.read() + .ok() + .is_some_and(|g| g.is_open() && g.seed_bytes().is_ok()) + }) + .count() + }) + .unwrap_or(0); + // Register reconcile channel BEFORE starting SPV so the event handlers + // (spawned inside run_spv_loop) always capture a valid sender. self.spv_setup_reconcile_listener(); + self.spv_setup_finality_listener(); + self.spv_manager.start(expected_wallets)?; Ok(()) } @@ -298,9 +325,6 @@ impl AppContext { let mut inserted = 0u32; for account in collection.all_accounts() { let account_type = account.account_type.to_account_type(); - if matches!(account_type, AccountType::Standard { .. }) { - continue; - } let Some((path_reference, path_type)) = Self::spv_account_metadata(&account_type) else { continue; @@ -376,6 +400,89 @@ impl AppContext { (default_ref, default_type) } + /// Listen for SPV instant lock / chain lock events and populate + /// transactions_waiting_for_finality so identity registration can proceed. + pub fn spv_setup_finality_listener(self: &Arc) { + let rx = self.spv_manager.register_finality_channel(); + let ctx = Arc::clone(self); + self.subtasks.spawn_sync(async move { + tokio::pin!(rx); + while let Some(event) = rx.recv().await { + if let Err(e) = ctx.handle_spv_finality_event(event).await { + tracing::debug!("SPV finality event error: {}", e); + } + } + }); + } + + async fn handle_spv_finality_event(&self, event: AssetLockFinalityEvent) -> Result<(), String> { + match event { + AssetLockFinalityEvent::InstantLock { txid, instant_lock } => { + // Check if this txid is pending in transactions_waiting_for_finality + let is_pending = { + let transactions = self.transactions_waiting_for_finality.lock().unwrap(); + matches!(transactions.get(&txid), Some(None)) + }; + if !is_pending { + return Ok(()); + } + + // Retrieve the full transaction from the database + let (tx, ..) = self + .db + .get_asset_lock_transaction(txid.as_byte_array()) + .map_err(|e| format!("DB error: {}", e))? + .ok_or_else(|| "Asset lock transaction not found in DB".to_string())?; + + self.received_asset_lock_finality(&tx, Some(*instant_lock), None) + .map_err(|e| format!("Finality processing error: {}", e))?; + } + AssetLockFinalityEvent::ChainLock { + height: _height, .. + } => { + // Get all pending txids (where proof is None) + let pending_txids: Vec = { + let transactions = self.transactions_waiting_for_finality.lock().unwrap(); + transactions + .iter() + .filter_map( + |(txid, proof)| if proof.is_none() { Some(*txid) } else { None }, + ) + .collect() + }; + if pending_txids.is_empty() { + return Ok(()); + } + + let sdk = { + let guard = self.sdk.read().map_err(|_| "SDK lock poisoned")?; + guard.clone() + }; + + for txid in pending_txids { + match get_transaction_info(&sdk, &txid).await { + Ok(tx_info) if tx_info.is_chain_locked && tx_info.height > 0 => { + if let Ok(Some((tx, ..))) = + self.db.get_asset_lock_transaction(txid.as_byte_array()) + { + let _ = self.received_asset_lock_finality( + &tx, + None, + Some(tx_info.height), + ); + } + } + _ => { + // Transaction not yet chain-locked at this height, or DAPI + // lookup failed — will retry on next chain lock event. + } + } + } + } + } + Ok(()) + } + /// Subscribe to SPV reconcile signals and debounce updates. pub fn spv_setup_reconcile_listener(self: &Arc) { use tokio::time::{Duration, Instant, sleep}; @@ -417,7 +524,7 @@ impl AppContext { let balance = wm .get_wallet_balance(wallet_id) .map_err(|e| format!("get_wallet_balance failed: {e}"))?; - tracing::debug!(wallet = %hex::encode(seed_hash), spendable = balance.spendable(), unconfirmed = balance.unconfirmed, total = balance.total, "SPV balance snapshot"); + tracing::debug!(wallet = %hex::encode(seed_hash), spendable = balance.spendable(), unconfirmed = balance.unconfirmed(), total = balance.total(), "SPV balance snapshot"); let Some(wallet_info) = wm.get_wallet_info(wallet_id) else { continue; @@ -430,13 +537,17 @@ impl AppContext { self.sync_spv_account_addresses(wallet_info, &wallet_arc); if let Ok(mut wallet) = wallet_arc.write() { - wallet.update_spv_balances(balance.spendable(), balance.unconfirmed, balance.total); + wallet.update_spv_balances( + balance.spendable(), + balance.unconfirmed(), + balance.total(), + ); // Persist balances to database if let Err(e) = self.db.update_wallet_balances( seed_hash, balance.spendable(), - balance.unconfirmed, - balance.total, + balance.unconfirmed(), + balance.total(), ) { tracing::warn!(wallet = %hex::encode(seed_hash), error = %e, "Failed to persist wallet balances"); } @@ -462,10 +573,16 @@ impl AppContext { .map_err(|e| format!("wallet_utxos failed: {e}"))?; let mut per_address_sum: std::collections::BTreeMap = Default::default(); + // Build in-memory UTXO map to update wallet model + let mut new_utxos: std::collections::HashMap< + Address, + std::collections::HashMap< + dash_sdk::dpp::dashcore::OutPoint, + dash_sdk::dpp::dashcore::TxOut, + >, + > = Default::default(); for u in utxos { - // Best-effort accessors for outpoint/txout; adjust if API differs - // Try field access (common struct layout): `outpoint` + `txout` let outpoint = u.outpoint; let tx_out = u.txout.clone(); @@ -475,6 +592,15 @@ impl AppContext { Err(_) => continue, }; + // Always track the UTXO in the in-memory map for correct balance calculation + new_utxos + .entry(address.clone()) + .or_default() + .insert(outpoint, tx_out.clone()); + + // Always count the UTXO value in per-address sum + *per_address_sum.entry(address.clone()).or_default() += tx_out.value; + // If address unknown to DET, try to register using SPV metadata if !known_addresses.contains(&address) { let collection = wallet_info.accounts(); @@ -483,10 +609,20 @@ impl AppContext { if let Some(ai) = acc.get_address_info(&address) { let account_type = acc.account_type.to_account_type(); let (path_reference, path_type) = - Self::spv_account_metadata(&account_type).unwrap_or(( - DerivationPathReference::BIP44, - DerivationPathType::CLEAR_FUNDS, - )); + Self::spv_account_metadata(&account_type).unwrap_or_else(|| { + let default_ref = if ai.path.is_bip44(self.network) { + DerivationPathReference::BIP44 + } else if ai.path.is_bip32() { + DerivationPathReference::BIP32 + } else { + tracing::warn!( + path = %ai.path, + "SPV address has unrecognized derivation path structure" + ); + DerivationPathReference::Unknown + }; + (default_ref, DerivationPathType::CLEAR_FUNDS) + }); if let Ok(inserted) = self.register_spv_address( &wallet_arc, @@ -504,11 +640,21 @@ impl AppContext { } } if !registered { - continue; + tracing::debug!( + wallet = %hex::encode(seed_hash), + address = %address, + value = tx_out.value, + "SPV UTXO address not registered in DET (counted in balance but not in address table)" + ); + // Still persist the UTXO to DB and delete stale entry first + let _ = self.db.execute( + "DELETE FROM utxos WHERE address = ? AND network = ?", + rusqlite::params![address.to_string(), self.network.to_string()], + ); } } - // Insert UTXO row + // Insert UTXO row into DB self.db .insert_utxo( outpoint.txid.as_ref(), @@ -519,15 +665,15 @@ impl AppContext { self.network, ) .map_err(|e| e.to_string())?; - - // Sum per address for balance update - *per_address_sum.entry(address).or_default() += tx_out.value; } - // Write per-address balances into DB and wallet model + // Write per-address balances and UTXOs into wallet model if let Some(wref) = wallets_guard.get(seed_hash) && let Ok(mut w) = wref.write() { + // Update in-memory UTXOs map + w.utxos = new_utxos; + for (addr, sum) in per_address_sum.into_iter() { // Update wallet and DB through model helper let _ = w.update_address_balance(&addr, sum, self); @@ -537,9 +683,9 @@ impl AppContext { let history = wm .wallet_transaction_history(wallet_id) .map_err(|e| format!("wallet_transaction_history failed: {e}"))?; - let wallet_transactions: Vec = history + let wallet_transactions: Vec = history .into_iter() - .map(|record| crate::model::wallet::WalletTransaction { + .map(|record| WalletTransaction { txid: record.txid, transaction: record.transaction.clone(), timestamp: record.timestamp, @@ -552,14 +698,27 @@ impl AppContext { }) .collect(); - self.db - .replace_wallet_transactions(seed_hash, &self.network, &wallet_transactions) - .map_err(|e| e.to_string())?; + tracing::info!( + wallet = %hex::encode(seed_hash), + spv_transactions = wallet_transactions.len(), + spv_spendable = balance.spendable(), + spv_total = balance.total(), + "SPV reconcile summary" + ); + + // Only replace transactions if SPV returned some, to avoid wiping + // previously persisted history when SPV hasn't populated history yet. + if !wallet_transactions.is_empty() { + self.db + .replace_wallet_transactions(seed_hash, &self.network, &wallet_transactions) + .map_err(|e| e.to_string())?; + } if let Some(wref) = wallets_guard.get(seed_hash) && let Ok(mut wallet) = wref.write() + && !wallet_transactions.is_empty() { - wallet.set_transactions(wallet_transactions.clone()); + wallet.set_transactions(wallet_transactions); } } diff --git a/src/model/qualified_identity/encrypted_key_storage.rs b/src/model/qualified_identity/encrypted_key_storage.rs index a19eed6d7..055b154a6 100644 --- a/src/model/qualified_identity/encrypted_key_storage.rs +++ b/src/model/qualified_identity/encrypted_key_storage.rs @@ -54,8 +54,8 @@ impl Encode for WalletDerivationPath { } } -impl Decode for WalletDerivationPath { - fn decode(decoder: &mut D) -> Result { +impl Decode for WalletDerivationPath { + fn decode>(decoder: &mut D) -> Result { // Decode `wallet_seed_hash` let wallet_seed_hash = WalletSeedHash::decode(decoder)?; @@ -92,8 +92,10 @@ impl Decode for WalletDerivationPath { } } -impl<'de, Context> BorrowDecode<'de, Context> for WalletDerivationPath { - fn borrow_decode>(decoder: &mut D) -> Result { +impl<'de, C> BorrowDecode<'de, C> for WalletDerivationPath { + fn borrow_decode>( + decoder: &mut D, + ) -> Result { // Decode `wallet_seed_hash` let wallet_seed_hash = WalletSeedHash::decode(decoder)?; diff --git a/src/model/qualified_identity/mod.rs b/src/model/qualified_identity/mod.rs index 4152843ab..510d10f0d 100644 --- a/src/model/qualified_identity/mod.rs +++ b/src/model/qualified_identity/mod.rs @@ -270,8 +270,8 @@ impl Encode for QualifiedIdentity { } // Implement Decode manually for QualifiedIdentity, excluding decrypted_wallets -impl Decode for QualifiedIdentity { - fn decode( +impl Decode for QualifiedIdentity { + fn decode>( decoder: &mut D, ) -> Result { Ok(Self { diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index f2c3209a9..c2939032c 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -99,6 +99,7 @@ pub trait DerivationPathHelpers { fn is_bip44(&self, network: Network) -> bool; fn is_bip44_external(&self, network: Network) -> bool; fn is_bip44_change(&self, network: Network) -> bool; + fn is_bip32(&self) -> bool; fn is_asset_lock_funding(&self, network: Network) -> bool; fn is_platform_payment(&self, network: Network) -> bool; fn bip44_account_index(&self) -> Option; @@ -139,6 +140,11 @@ impl DerivationPathHelpers for DerivationPath { components.len() >= 5 && components[3] == ChildNumber::Normal { index: 1 } } + fn is_bip32(&self) -> bool { + let components = self.as_ref(); + matches!(components.len(), 2..=3) && components[0] == ChildNumber::Hardened { index: 0 } + } + fn is_asset_lock_funding(&self, network: Network) -> bool { let coin_type = match network { Network::Dash => 5, @@ -1807,6 +1813,37 @@ impl Wallet { .map_err(|e| e.to_string()) } + /// Recalculate and persist balances for all addresses affected by spent UTXOs. + /// + /// Call this after removing entries from `self.utxos` to keep `address_balances` + /// and the database in sync. + pub fn recalculate_affected_address_balances( + &mut self, + used_utxos: &BTreeMap, + context: &AppContext, + ) -> Result<(), String> { + let affected_addresses: BTreeSet<_> = + used_utxos.values().map(|(_, addr)| addr.clone()).collect(); + for address in affected_addresses { + self.recalculate_address_balance(&address, context)?; + } + Ok(()) + } + + /// Recalculate and persist the balance for a single address from its remaining UTXOs. + pub fn recalculate_address_balance( + &mut self, + address: &Address, + context: &AppContext, + ) -> Result<(), String> { + let new_balance = self + .utxos + .get(address) + .map(|utxo_map| utxo_map.values().map(|tx_out| tx_out.value).sum()) + .unwrap_or(0); + self.update_address_balance(address, new_balance, context) + } + pub fn update_address_total_received( &mut self, address: &Address, diff --git a/src/spv/manager.rs b/src/spv/manager.rs index ebad6b7ee..c03812a74 100644 --- a/src/spv/manager.rs +++ b/src/spv/manager.rs @@ -4,19 +4,23 @@ use crate::config::NetworkConfig; use crate::model::wallet::WalletSeedHash; use crate::utils::tasks::TaskManager; use dash_sdk::dash_spv::client::interface::{DashSpvClientCommand, DashSpvClientInterface}; +use dash_sdk::dash_spv::network::NetworkEvent; use dash_sdk::dash_spv::network::PeerNetworkManager; use dash_sdk::dash_spv::storage::DiskStorageManager; -use dash_sdk::dash_spv::types::{ - DetailedSyncProgress, SpvEvent, SyncProgress, SyncStage, ValidationMode, -}; +use dash_sdk::dash_spv::sync::SyncEvent; +use dash_sdk::dash_spv::sync::SyncProgress as WatchSyncProgress; +use dash_sdk::dash_spv::sync::SyncState; +use dash_sdk::dash_spv::types::{DetailedSyncProgress, SyncProgress, SyncStage, ValidationMode}; use dash_sdk::dash_spv::{ClientConfig, DashSpvClient, Hash, LLMQType, QuorumHash}; -use dash_sdk::dpp::dashcore::{Address, Network, Transaction}; +use dash_sdk::dpp::dashcore::{Address, InstantLock, Network, Transaction, Txid}; use dash_sdk::dpp::key_wallet::bip32::{DerivationPath, ExtendedPrivKey}; use dash_sdk::dpp::key_wallet::wallet::initialization::WalletAccountCreationOptions; use dash_sdk::dpp::key_wallet::wallet::managed_wallet_info::{ ManagedWalletInfo, transaction_building::AccountTypePreference, wallet_info_interface::WalletInfoInterface, }; +use dash_sdk::dpp::key_wallet_manager::WalletEvent; +use dash_sdk::dpp::key_wallet_manager::wallet_interface::WalletInterface; use dash_sdk::dpp::key_wallet_manager::wallet_manager::{WalletError, WalletId, WalletManager}; // use dash_sdk::dpp::key_wallet::bip32::ExtendedPubKey; // not needed directly here use std::fmt; @@ -86,12 +90,24 @@ pub struct SpvStatusSnapshot { pub last_error: Option, pub started_at: Option, pub last_updated: Option, + pub connected_peers: usize, } /// Type alias for the SPV client with our specific configuration type SpvClient = DashSpvClient, PeerNetworkManager, DiskStorageManager>; +/// Events forwarded from SPV to AppContext for asset lock proof construction. +pub(crate) enum AssetLockFinalityEvent { + InstantLock { + txid: Txid, + instant_lock: Box, + }, + ChainLock { + height: u32, + }, +} + /// Manages SPV client lifecycle and exposes status updates. /// Uses dash-spv's built-in state management while maintaining a dedicated runtime for performance. /// @@ -117,6 +133,8 @@ pub struct SpvManager { det_wallets: Arc>>, // signal channel to trigger external reconcile on wallet-related events reconcile_tx: Mutex>>, + // signal channel to forward instant lock / chain lock events for asset lock proof construction + finality_tx: Mutex>>, // Whether to use local Dash Core node instead of DNS seed discovery use_local_node: Arc, // Cancellation token for clean shutdown @@ -125,6 +143,8 @@ pub struct SpvManager { request_tx: Mutex>>, // Network manager clone for broadcasting transactions (set when client is running) network_manager: Arc>>, + // Number of currently connected SPV peers + connected_peers: Arc>, } /// Requests that can be sent to the SPV runtime thread @@ -275,10 +295,12 @@ impl SpvManager { progress_updated_at: Arc::new(RwLock::new(None)), det_wallets: Arc::new(RwLock::new(std::collections::BTreeMap::new())), reconcile_tx: Mutex::new(None), + finality_tx: Mutex::new(None), use_local_node: Arc::new(AtomicBool::new(false)), stop_token: Mutex::new(None), request_tx: Mutex::new(None), network_manager: Arc::new(AsyncRwLock::new(None)), + connected_peers: Arc::new(RwLock::new(0)), }); Ok(manager) @@ -307,6 +329,7 @@ impl SpvManager { .read_progress_updated_at() .unwrap_or(None) .or(Some(SystemTime::now())); + let connected_peers = self.connected_peers.read().map(|g| *g).unwrap_or(0); SpvStatusSnapshot { status, @@ -315,6 +338,7 @@ impl SpvManager { last_error, started_at, last_updated, + connected_peers, } } @@ -330,6 +354,7 @@ impl SpvManager { .read_progress_updated_at() .unwrap_or(None) .or(Some(SystemTime::now())); + let connected_peers = self.connected_peers.read().map(|g| *g).unwrap_or(0); SpvStatusSnapshot { status, @@ -338,10 +363,11 @@ impl SpvManager { last_error, started_at, last_updated, + connected_peers, } } - pub fn start(self: &Arc) -> Result<(), String> { + pub fn start(self: &Arc, expected_wallet_count: usize) -> Result<(), String> { // Check if already running { let stop_token_guard = self @@ -390,7 +416,7 @@ impl SpvManager { rt.block_on(async move { let manager_for_loop = Arc::clone(&manager); - if let Err(err) = manager_for_loop.run_spv_loop(stop_token, global_cancel).await { + if let Err(err) = manager_for_loop.run_spv_loop(stop_token, global_cancel, expected_wallet_count).await { tracing::error!(error = %err, network = ?manager.network, "SPV runtime failed"); if let Err(e) = manager.write_last_error(Some(err.clone())) { tracing::error!("Failed to write SPV error: {}", e); @@ -488,6 +514,9 @@ impl SpvManager { /// Create a reconciliation signal channel for external listeners. /// Returns a receiver that will get a signal when SPV wallet state likely changed. + /// + /// Only one subscriber is supported at a time. Calling this again replaces + /// the previous sender, so the earlier receiver will stop receiving signals. pub fn register_reconcile_channel(&self) -> mpsc::Receiver<()> { let (tx, rx) = mpsc::channel(64); if let Ok(mut guard) = self.reconcile_tx.lock() { @@ -496,6 +525,19 @@ impl SpvManager { rx } + /// Create a finality event channel for external listeners. + /// Returns a receiver that will get events when SPV detects instant locks or chain locks. + /// + /// Only one subscriber is supported at a time. Calling this again replaces + /// the previous sender, so the earlier receiver will stop receiving events. + pub(crate) fn register_finality_channel(&self) -> mpsc::Receiver { + let (tx, rx) = mpsc::channel(64); + if let Ok(mut guard) = self.finality_tx.lock() { + *guard = Some(tx); + } + rx + } + /// Remove all cached SPV data on disk for the current network. /// /// This requires the SPV runtime to be stopped first; otherwise the @@ -522,6 +564,17 @@ impl SpvManager { wallet_map.clear(); } + // Reset the in-memory WalletManager's synced_height so the next SPV session + // scans filters from genesis instead of the stale height from the previous run. + match self.wallet.try_write() { + Ok(mut wm) => { + wm.update_synced_height(0); + } + Err(_) => { + tracing::warn!("Failed to reset WalletManager synced_height during SPV data clear"); + } + } + self.write_sync_progress(None).map_err(|e| e.to_string())?; self.write_detailed_progress(None) .map_err(|e| e.to_string())?; @@ -531,6 +584,9 @@ impl SpvManager { self.write_last_error(None).map_err(|e| e.to_string())?; self.write_status(SpvStatus::Idle) .map_err(|e| e.to_string())?; + if let Ok(mut guard) = self.connected_peers.write() { + *guard = 0; + } if self.data_dir.exists() { fs::remove_dir_all(&self.data_dir).map_err(|e| { @@ -726,9 +782,49 @@ impl SpvManager { self: Arc, stop_token: CancellationToken, global_cancel: CancellationToken, + expected_wallet_count: usize, ) -> Result<(), String> { + // Wait for all expected wallets to be fully loaded into the WalletManager + // before building the client. Wallet loading happens via queue_spv_wallet_load + // which calls import_wallet_from_extended_priv_key (derives all accounts and + // addresses) under a write lock. Once wallet_count() reaches the expected + // value, all addresses are derived and monitored_addresses() is populated. + if expected_wallet_count > 0 { + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(30); + loop { + { + let wm = self.wallet.read().await; + let loaded = wm.wallet_count(); + if loaded >= expected_wallet_count { + let addr_count = wm.monitored_addresses().len(); + tracing::info!( + expected = expected_wallet_count, + loaded, + addresses = addr_count, + "SPV: all wallets loaded with monitored addresses, proceeding" + ); + break; + } + } + if tokio::time::Instant::now() >= deadline { + let wm = self.wallet.read().await; + tracing::warn!( + expected = expected_wallet_count, + loaded = wm.wallet_count(), + "SPV: timed out waiting for all wallets to load, proceeding anyway" + ); + break; + } + if stop_token.is_cancelled() || global_cancel.is_cancelled() { + return Ok(()); + } + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + } + // Build and start the client - let mut client = self.build_client().await?; + let has_wallets = expected_wallet_count > 0; + let mut client = self.build_client(has_wallets).await?; client .start() .await @@ -742,16 +838,25 @@ impl SpvManager { } } - // Set up progress handler - if let Some(progress_rx) = client.take_progress_receiver() { - self.spawn_progress_handler(progress_rx); - } + // Subscribe to sync events (broadcast) + let sync_rx = client.subscribe_sync_events(); + self.spawn_sync_event_handler(sync_rx); - // Set up event handler - if let Some(event_rx) = client.take_event_receiver() { - self.spawn_event_handler(event_rx); + // Subscribe to wallet events (broadcast from WalletManager) + { + let wm = self.wallet.read().await; + let wallet_rx = wm.subscribe_events(); + self.spawn_wallet_event_handler(wallet_rx); } + // Subscribe to network events (broadcast) + let net_rx = client.subscribe_network_events(); + self.spawn_network_event_handler(net_rx); + + // Set up progress handler using watch channel + let progress_rx = client.subscribe_progress(); + self.spawn_progress_watcher(progress_rx); + // Set up request handler with access to shared components let (request_tx, request_rx) = mpsc::channel(32); { @@ -796,6 +901,9 @@ impl SpvManager { let mut nm_guard = self.network_manager.write().await; *nm_guard = None; } + if let Ok(mut guard) = self.connected_peers.write() { + *guard = 0; + } { // Drop shared storage/request handles so the disk lock is released before restart. if let Ok(mut storage_guard) = self.storage.lock() { @@ -922,73 +1030,226 @@ impl SpvManager { }); } - fn spawn_progress_handler( + fn spawn_progress_watcher( &self, - mut progress_rx: tokio::sync::mpsc::UnboundedReceiver, + mut progress_rx: tokio::sync::watch::Receiver, ) { let status = Arc::clone(&self.status); - let last_error = Arc::clone(&self.last_error); let sync_progress_state = Arc::clone(&self.sync_progress_state); let detailed_progress_state = Arc::clone(&self.detailed_progress_state); let progress_updated_at = Arc::clone(&self.progress_updated_at); let cancel = self.subtasks.cancellation_token.clone(); self.subtasks.spawn_sync(async move { - let mut last_update = std::time::Instant::now(); - let min_interval = std::time::Duration::from_millis(500); + loop { + tokio::select! { + _ = cancel.cancelled() => break, + result = progress_rx.changed() => { + if result.is_err() { + break; // Channel closed + } + let watch_progress = progress_rx.borrow(); + + // Extract all available heights from WatchSyncProgress + let header_height = watch_progress + .headers() + .map(|h| h.current_height()) + .unwrap_or(0); + let masternode_height = watch_progress + .masternodes() + .map(|m| m.current_height()) + .unwrap_or(0); + let filter_header_height = watch_progress + .filter_headers() + .map(|fh| fh.current_height()) + .unwrap_or(0); + + let sync_progress = SyncProgress { + header_height, + masternode_height, + filter_header_height, + ..Default::default() + }; + + // Build detailed progress with sync stage information + let peer_best_height = watch_progress + .headers() + .map(|h| h.target_height()) + .unwrap_or(0); + let sync_stage = Self::determine_sync_stage(&watch_progress); + let detailed = DetailedSyncProgress { + sync_progress: sync_progress.clone(), + peer_best_height, + percentage: if peer_best_height > 0 { + (header_height as f64 / peer_best_height as f64 * 100.0).min(100.0) + } else { + 0.0 + }, + headers_per_second: 0.0, + bytes_per_second: 0, + estimated_time_remaining: None, + sync_stage, + total_headers_processed: 0, + total_bytes_downloaded: 0, + sync_start_time: SystemTime::now(), + last_update_time: SystemTime::now(), + }; + + // Update sync progress state + if let Ok(mut stored_sync) = sync_progress_state.write() { + *stored_sync = Some(sync_progress); + } + if let Ok(mut stored_detailed) = detailed_progress_state.write() { + *stored_detailed = Some(detailed); + } + if let Ok(mut updated_at) = progress_updated_at.write() { + *updated_at = Some(SystemTime::now()); + } + + // Update status based on progress + if let Ok(mut status_guard) = status.write() { + if watch_progress.is_synced() { + *status_guard = SpvStatus::Running; + } else if !matches!(*status_guard, SpvStatus::Stopping | SpvStatus::Stopped | SpvStatus::Error) { + *status_guard = SpvStatus::Syncing; + } + } + } + } + } + tracing::info!("SPV progress watcher exiting"); + }); + } + + /// Map the parallel WatchSyncProgress managers into a single UI-facing SyncStage. + /// + /// The parallel sync system has independent managers for headers, masternodes, + /// filter-headers, filters, and blocks. We map to a single stage by checking + /// which manager is actively syncing, preferring later pipeline stages. + fn determine_sync_stage(watch: &WatchSyncProgress) -> SyncStage { + // Check stages from latest to earliest in the pipeline + if let Ok(blocks) = watch.blocks() + && blocks.state() == SyncState::Syncing + { + return SyncStage::DownloadingBlocks { + pending: blocks.requested().saturating_sub(blocks.processed()) as usize, + }; + } + if let Ok(filters) = watch.filters() + && filters.state() == SyncState::Syncing + { + return SyncStage::DownloadingFilters { + completed: filters.downloaded(), + total: filters + .target_height() + .saturating_sub(filters.current_height()), + }; + } + if let Ok(fh) = watch.filter_headers() + && fh.state() == SyncState::Syncing + { + return SyncStage::DownloadingFilterHeaders { + current: fh.current_height(), + target: fh.target_height(), + }; + } + if let Ok(mn) = watch.masternodes() + && mn.state() == SyncState::Syncing + { + return SyncStage::ValidatingHeaders { + batch_size: mn.diffs_processed() as usize, + }; + } + if let Ok(headers) = watch.headers() + && headers.state() == SyncState::Syncing + { + return SyncStage::DownloadingHeaders { + start: 0, + end: headers.target_height(), + }; + } + + if watch.is_synced() { + SyncStage::Complete + } else { + SyncStage::Connecting + } + } + fn spawn_sync_event_handler(&self, mut sync_rx: tokio::sync::broadcast::Receiver) { + let reconcile_tx = self.reconcile_tx.lock().ok().and_then(|g| g.clone()); + let finality_tx = self.finality_tx.lock().ok().and_then(|g| g.clone()); + let status = Arc::clone(&self.status); + let cancel = self.subtasks.cancellation_token.clone(); + + self.subtasks.spawn_sync(async move { loop { tokio::select! { _ = cancel.cancelled() => break, - msg = progress_rx.recv() => { - match msg { - Some(detailed) => { - if let Ok(mut stored_detailed) = detailed_progress_state.write() { - *stored_detailed = Some(detailed.clone()); - } - if let Ok(mut stored_sync) = sync_progress_state.write() { - *stored_sync = Some(detailed.sync_progress.clone()); - } - if let Ok(mut updated_at) = progress_updated_at.write() { - *updated_at = Some(detailed.last_update_time); - } + result = sync_rx.recv() => { + match result { + Ok(event) => { + let should_signal = matches!( + event, + SyncEvent::BlockProcessed { .. } + | SyncEvent::ChainLockReceived { .. } + | SyncEvent::InstantLockReceived { .. } + | SyncEvent::SyncComplete { .. } + ); - if last_update.elapsed() >= min_interval { - // Update status based on progress stage and completeness - if let Ok(mut status_guard) = status.write() { - let current = *status_guard; - match &detailed.sync_stage { - SyncStage::Complete => { - *status_guard = SpvStatus::Running; - } - SyncStage::Failed(message) => { - *status_guard = SpvStatus::Error; - if let Ok(mut err_guard) = last_error.write() { - *err_guard = Some(format!("SPV sync failed: {message}")); - } + // Forward finality-relevant events for asset lock proof construction + if let Some(ref ftx) = finality_tx { + match &event { + SyncEvent::InstantLockReceived { instant_lock, .. } => { + if let Err(e) = ftx.try_send(AssetLockFinalityEvent::InstantLock { + txid: instant_lock.txid, + instant_lock: Box::new(instant_lock.clone()), + }) { + tracing::warn!("Failed to forward InstantLock finality event for txid {}: {}", instant_lock.txid, e); } - _ => { - if !matches!( - current, - SpvStatus::Stopping | SpvStatus::Stopped | SpvStatus::Error - ) { - *status_guard = SpvStatus::Syncing; - } + } + SyncEvent::ChainLockReceived { chain_lock, .. } => { + if let Err(e) = ftx.try_send(AssetLockFinalityEvent::ChainLock { + height: chain_lock.block_height, + }) { + tracing::warn!("Failed to forward ChainLock finality event for height {}: {}", chain_lock.block_height, e); } } + _ => {} } - last_update = std::time::Instant::now(); + } + + if matches!(event, SyncEvent::SyncComplete { .. }) + && let Ok(mut guard) = status.write() + { + *guard = SpvStatus::Running; + } + if should_signal + && let Some(ref tx) = reconcile_tx + { + let _ = tx.try_send(()); } } - None => break, + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + tracing::warn!("Sync event handler lagged by {} events", n); + // Trigger reconcile to catch up on any missed state changes + if let Some(ref tx) = reconcile_tx { + let _ = tx.try_send(()); + } + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, } } } } + tracing::info!("SPV sync event handler exiting"); }); } - fn spawn_event_handler(&self, mut event_rx: tokio::sync::mpsc::UnboundedReceiver) { + fn spawn_wallet_event_handler( + &self, + mut wallet_rx: tokio::sync::broadcast::Receiver, + ) { let reconcile_tx = self.reconcile_tx.lock().ok().and_then(|g| g.clone()); let cancel = self.subtasks.cancellation_token.clone(); @@ -996,41 +1257,79 @@ impl SpvManager { loop { tokio::select! { _ = cancel.cancelled() => break, - evt = event_rx.recv() => { - match evt { - Some(event) => { - // Push reconcile signal for wallet-related updates - let should_signal = matches!(event, - SpvEvent::TransactionDetected { .. } | - SpvEvent::BalanceUpdate { .. } | - SpvEvent::BlockProcessed { .. } - ); - if should_signal - && let Some(ref tx) = reconcile_tx { + result = wallet_rx.recv() => { + match result { + Ok(_event) => { + // All wallet events trigger reconcile + if let Some(ref tx) = reconcile_tx { let _ = tx.try_send(()); } } - None => break, + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + tracing::warn!("Wallet event handler lagged by {} events", n); + // Still trigger reconcile to catch up + if let Some(ref tx) = reconcile_tx { + let _ = tx.try_send(()); + } + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, + } + } + } + } + tracing::info!("SPV wallet event handler exiting"); + }); + } + + fn spawn_network_event_handler( + &self, + mut net_rx: tokio::sync::broadcast::Receiver, + ) { + let connected_peers = Arc::clone(&self.connected_peers); + let cancel = self.subtasks.cancellation_token.clone(); + + self.subtasks.spawn_sync(async move { + loop { + tokio::select! { + _ = cancel.cancelled() => break, + result = net_rx.recv() => { + match result { + Ok(NetworkEvent::PeersUpdated { connected_count, .. }) => { + if let Ok(mut guard) = connected_peers.write() { + *guard = connected_count; + } + } + Ok(_) => { + // PeerConnected / PeerDisconnected — PeersUpdated follows + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + tracing::warn!("Network event handler lagged by {} events", n); + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, } } } } + tracing::info!("SPV network event handler exiting"); }); } async fn build_client( &self, + has_wallets: bool, ) -> Result< DashSpvClient, PeerNetworkManager, DiskStorageManager>, String, > { - let start_height = { - let guard = self.wallet.read().await; - if guard.wallet_count() == 0 { - u32::MAX - } else { - 0 - } + // When wallets exist, scan from genesis so historical transactions are found via + // compact block filters. When no wallets are loaded, skip to chain tip (u32::MAX) + // to avoid unnecessary work. We check both the caller hint and the actual wallet + // count (the wait loop in run_spv_loop should have ensured loading completed). + let wallet_count = self.wallet.read().await.wallet_count(); + let start_height = if has_wallets || wallet_count > 0 { + 0 + } else { + u32::MAX }; let mut config = ClientConfig::new(self.network) .with_storage_path(self.data_dir.clone()) @@ -1063,7 +1362,7 @@ impl SpvManager { *nm_guard = Some(network_manager.clone()); } - let storage_manager = DiskStorageManager::new(self.data_dir.clone()) + let storage_manager = DiskStorageManager::new(&config) .await .map_err(|e| format!("Failed to initialize SPV storage: {e}"))?; diff --git a/src/spv/mod.rs b/src/spv/mod.rs index 3eebea869..022d2c569 100644 --- a/src/spv/mod.rs +++ b/src/spv/mod.rs @@ -1,5 +1,6 @@ mod error; -mod manager; +pub(crate) mod manager; pub use error::{SpvError, SpvResult}; +pub(crate) use manager::AssetLockFinalityEvent; pub use manager::{CoreBackendMode, SpvDerivedAddress, SpvManager, SpvStatus, SpvStatusSnapshot}; diff --git a/src/ui/helpers.rs b/src/ui/helpers.rs index e12bd1dbb..77bded3af 100644 --- a/src/ui/helpers.rs +++ b/src/ui/helpers.rs @@ -11,7 +11,11 @@ use arboard::Clipboard; use dash_sdk::{ dpp::{ data_contract::{ + GroupContractPosition, accessors::v0::DataContractV0Getters, + accessors::v1::DataContractV1Getters, + associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters, + change_control_rules::authorized_action_takers::AuthorizedActionTakers, document_type::{DocumentType, accessors::DocumentTypeV0Getters}, group::{Group, accessors::v0::GroupV0Getters}, }, @@ -31,6 +35,174 @@ use super::tokens::tokens_screen::IdentityTokenInfo; /// This constant provides a constant padding to be used in such cases to ensure proper alignment. pub const BUTTON_ADJUSTMENT_PADDING_TOP: f32 = 15.0; +/// Formats a key label for display in combo boxes and lists. +/// Returns a string like "Key 0 | AUTHENTICATION | CRITICAL | ECDSA_SECP256K1" +pub fn format_key_label(key: &IdentityPublicKey) -> String { + format!( + "Key {} | {} | {} | {}", + key.id(), + key.purpose(), + key.security_level(), + key.key_type() + ) +} + +/// Formats a key label with a [DEV] suffix for dev mode display. +pub fn format_key_label_dev(key: &IdentityPublicKey) -> String { + format!( + "Key {} | {} | {} | {} [DEV]", + key.id(), + key.purpose(), + key.security_level(), + key.key_type() + ) +} + +/// Returns the display label for a QualifiedIdentity (alias or Base58 ID). +pub fn identity_display_label(identity: &QualifiedIdentity) -> String { + identity + .alias + .clone() + .unwrap_or_else(|| identity.identity.id().to_string(Encoding::Base58)) +} + +/// Returns the display label for a QualifiedContract (alias or Base58 ID). +pub fn contract_display_label(contract: &QualifiedContract) -> String { + contract + .alias + .clone() + .unwrap_or_else(|| contract.contract.id().to_string(Encoding::Base58)) +} + +/// Computes the allowed security levels for a given transaction type and optional document type. +/// This centralizes the logic that was previously duplicated in multiple places. +pub fn compute_allowed_security_levels( + transaction_type: TransactionType, + document_type: Option<&DocumentType>, +) -> Vec { + match (transaction_type, document_type) { + (TransactionType::DocumentAction, Some(doc_type)) => { + let required_level = doc_type.security_level_requirement(); + let allowed_range = SecurityLevel::CRITICAL as u8..=required_level as u8; + [ + SecurityLevel::CRITICAL, + SecurityLevel::HIGH, + SecurityLevel::MEDIUM, + ] + .into_iter() + .filter(|level| allowed_range.contains(&(*level as u8))) + .collect() + } + _ => transaction_type.allowed_security_levels(), + } +} + +/// Result of checking token action authorization. +/// Contains the group if applicable and any error message. +pub struct TokenAuthorizationResult { + pub group: Option<(GroupContractPosition, Group)>, + pub error_message: Option, + pub is_unilateral_group_member: bool, +} + +/// Checks if the given identity is authorized to perform a token action. +/// This centralizes the authorization checking logic used across token screens (mint, burn, pause, etc.). +/// +/// # Arguments +/// * `action_takers` - The authorized action takers for the operation +/// * `identity_token_info` - The token and identity information +/// * `action_name` - Human-readable name of the action (e.g., "mint", "burn") +/// +/// # Returns +/// A `TokenAuthorizationResult` containing the group (if applicable), any error message, +/// and whether the user is a unilateral group member. +pub fn check_token_authorization( + action_takers: &AuthorizedActionTakers, + identity_token_info: &IdentityTokenInfo, + action_name: &str, +) -> TokenAuthorizationResult { + let mut error_message = None; + + let group = match action_takers { + AuthorizedActionTakers::NoOne => { + error_message = Some(format!("{} is not allowed on this token", action_name)); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != identity_token_info.identity.identity.id() + { + error_message = Some(format!( + "You are not allowed to {} this token. Only the contract owner is.", + action_name.to_lowercase() + )); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some(format!( + "You are not allowed to {} this token", + action_name.to_lowercase() + )); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + let is_unilateral_group_member = if let Some((_, ref g)) = group { + g.members() + .get(&identity_token_info.identity.identity.id()) + .map(|power| *power >= g.required_power()) + .unwrap_or(false) + } else { + false + }; + + TokenAuthorizationResult { + group, + error_message, + is_unilateral_group_member, + } +} + /// Helper function to create a styled info icon button with a circle and "i" /// Returns a Response that can be checked for .clicked() to show an info popup pub fn info_icon_button(ui: &mut egui::Ui, hover_text: &str) -> Response { @@ -220,21 +392,7 @@ pub fn add_key_chooser_with_doc_type( let mut action = AppAction::None; let allowed_purposes = transaction_type.allowed_purposes(); - let allowed_security_levels: Vec = match (transaction_type, document_type) { - (TransactionType::DocumentAction, Some(doc_type)) => { - let required_level = doc_type.security_level_requirement(); - let allowed_levels = SecurityLevel::CRITICAL as u8..=required_level as u8; - [ - SecurityLevel::CRITICAL, - SecurityLevel::HIGH, - SecurityLevel::MEDIUM, - ] - .into_iter() - .filter(|level| allowed_levels.contains(&(*level as u8))) - .collect() - } - _ => transaction_type.allowed_security_levels(), - }; + let allowed_security_levels = compute_allowed_security_levels(transaction_type, document_type); // Check for keys with private keys loaded let has_suitable_keys_with_private = @@ -300,48 +458,25 @@ pub fn add_key_chooser_with_doc_type( .selected_text( selected_key .as_ref() - .map(|k| { - format!( - "Key {} | {} | {} | {}", - k.id(), - k.purpose(), - k.security_level(), - k.key_type() - ) - }) + .map(format_key_label) .unwrap_or_else(|| "Select Key...".into()), ) .show_ui(ui, |kui| { for key_ref in identity.private_keys.identity_public_keys() { let key = &key_ref.1.identity_public_key; - let is_allowed = if is_dev_mode { - true - } else { - allowed_purposes.contains(&key.purpose()) - && allowed_security_levels.contains(&key.security_level()) - }; + let is_allowed = is_dev_mode + || (allowed_purposes.contains(&key.purpose()) + && allowed_security_levels.contains(&key.security_level())); if is_allowed { - let label = if is_dev_mode + let is_dev_override = is_dev_mode && (!allowed_purposes.contains(&key.purpose()) - || !allowed_security_levels.contains(&key.security_level())) - { - format!( - "Key {} | {} | {} | {} [DEV]", - key.id(), - key.purpose(), - key.security_level(), - key.key_type() - ) + || !allowed_security_levels.contains(&key.security_level())); + let label = if is_dev_override { + format_key_label_dev(key) } else { - format!( - "Key {} | {} | {} | {}", - key.id(), - key.purpose(), - key.security_level(), - key.key_type() - ) + format_key_label(key) }; if kui @@ -407,23 +542,19 @@ where ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { ComboBox::from_id_salt("identity_combo") .width(220.0) - .selected_text(match selected_identity { - Some(qi) => qi - .alias - .clone() - .unwrap_or_else(|| qi.identity.id().to_string(Encoding::Base58)), - None => "Select Identity…".into(), - }) + .selected_text( + selected_identity + .as_ref() + .map(identity_display_label) + .unwrap_or_else(|| "Select Identity…".into()), + ) .show_ui(ui, |iui| { for qi in identities { - let label = qi - .alias - .clone() - .unwrap_or_else(|| qi.identity.id().to_string(Encoding::Base58)); + let label = identity_display_label(qi); if iui .selectable_label( selected_identity.as_ref() == Some(qi), - label.clone(), + label, ) .clicked() { @@ -443,17 +574,8 @@ where let mut show_combo = true; if let Some(qi) = selected_identity { let allowed_purposes = transaction_type.allowed_purposes(); - let allowed_security_levels: Vec = match (transaction_type, document_type) { - (TransactionType::DocumentAction, Some(doc_type)) => { - let required_level = doc_type.security_level_requirement(); - let allowed_levels = SecurityLevel::CRITICAL as u8..=required_level as u8; - [SecurityLevel::CRITICAL, SecurityLevel::HIGH, SecurityLevel::MEDIUM] - .into_iter() - .filter(|level| allowed_levels.contains(&(*level as u8))) - .collect() - } - _ => transaction_type.allowed_security_levels(), - }; + let allowed_security_levels = + compute_allowed_security_levels(transaction_type, document_type); // Check for keys with private keys loaded let has_suitable_keys_with_private = qi @@ -520,100 +642,49 @@ where } if show_combo { - ComboBox::from_id_salt("key_combo") - .width(220.0) - .selected_text( - selected_key - .as_ref() - .map(|k| { - format!( - "Key {} | {} | {} | {}", - k.id(), - k.purpose(), - k.security_level(), - k.key_type() - ) - }) - .unwrap_or_else(|| "Select Key…".into()), - ) - .show_ui(ui, |kui| { - if let Some(qi) = selected_identity { - let allowed_purposes = transaction_type.allowed_purposes(); - let allowed_security_levels = if transaction_type - == TransactionType::DocumentAction - { - if let Some(document_type) = document_type { - // For document actions with a specific document type, use its security requirement - let required_level = document_type.security_level_requirement(); - let allowed_levels = - SecurityLevel::CRITICAL as u8..=required_level as u8; - let allowed_levels: Vec = [ - SecurityLevel::CRITICAL, - SecurityLevel::HIGH, - SecurityLevel::MEDIUM, - ] - .iter() - .cloned() - .filter(|level| allowed_levels.contains(&(*level as u8))) - .collect(); - allowed_levels - } else { - transaction_type.allowed_security_levels() - } - } else { - transaction_type.allowed_security_levels() - }; - - for key_ref in qi.private_keys.identity_public_keys() { - let key = &key_ref.1.identity_public_key; - - // In dev mode, show all keys - // In production mode, filter by transaction requirements - let is_allowed = if is_dev_mode { - true - } else { - allowed_purposes - .contains(&key.purpose()) - && allowed_security_levels.contains(&key.security_level()) - }; - - if is_allowed { - let label = if is_dev_mode - && (!allowed_purposes.contains(&key.purpose()) - || !allowed_security_levels - .contains(&key.security_level())) - { - // In dev mode, mark keys that wouldn't normally be allowed - format!( - "Key {} | {} | {} | {} [DEV]", - key.id(), - key.purpose(), - key.security_level(), - key.key_type() - ) - } else { - format!( - "Key {} | {} | {} | {}", - key.id(), - key.purpose(), - key.security_level(), - key.key_type() - ) - }; - - if kui - .selectable_label(selected_key.as_ref() == Some(key), label) - .clicked() - { - *selected_key = Some(key.clone()); + ComboBox::from_id_salt("key_combo") + .width(220.0) + .selected_text( + selected_key + .as_ref() + .map(format_key_label) + .unwrap_or_else(|| "Select Key…".into()), + ) + .show_ui(ui, |kui| { + if let Some(qi) = selected_identity { + let allowed_purposes = transaction_type.allowed_purposes(); + let allowed_security_levels = + compute_allowed_security_levels(transaction_type, document_type); + + for key_ref in qi.private_keys.identity_public_keys() { + let key = &key_ref.1.identity_public_key; + + let is_allowed = is_dev_mode + || (allowed_purposes.contains(&key.purpose()) + && allowed_security_levels.contains(&key.security_level())); + + if is_allowed { + let is_dev_override = is_dev_mode + && (!allowed_purposes.contains(&key.purpose()) + || !allowed_security_levels.contains(&key.security_level())); + let label = if is_dev_override { + format_key_label_dev(key) + } else { + format_key_label(key) + }; + + if kui + .selectable_label(selected_key.as_ref() == Some(key), label) + .clicked() + { + *selected_key = Some(key.clone()); + } } } + } else { + kui.label("Pick an identity first"); } - - } else { - kui.label("Pick an identity first"); - } - }); + }); } }); ui.end_row(); @@ -632,11 +703,9 @@ pub fn add_contract_doc_type_chooser_with_filtering( let contracts = app_context.get_contracts(None, None).unwrap_or_default(); let search_term_lowercase = search_term.to_lowercase(); let filtered = contracts.iter().filter(|qc| { - let key = qc - .alias - .clone() - .unwrap_or_else(|| qc.contract.id().to_string(Encoding::Base58)); - key.to_lowercase().contains(&search_term_lowercase) + contract_display_label(qc) + .to_lowercase() + .contains(&search_term_lowercase) }); add_contract_doc_type_chooser_pre_filtered( @@ -675,24 +744,17 @@ pub fn add_contract_doc_type_chooser_pre_filtered<'a, T>( ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { ComboBox::from_id_salt("contract_combo") .width(220.0) - .selected_text(match selected_contract { - Some(qc) => qc - .alias - .clone() - .unwrap_or_else(|| qc.contract.id().to_string(Encoding::Base58)), - None => "Select Contract…".into(), - }) + .selected_text( + selected_contract + .as_ref() + .map(contract_display_label) + .unwrap_or_else(|| "Select Contract…".into()), + ) .show_ui(ui, |cui| { for qc in filtered_contracts { - let label = qc - .alias - .clone() - .unwrap_or_else(|| qc.contract.id().to_string(Encoding::Base58)); + let label = contract_display_label(qc); if cui - .selectable_label( - selected_contract.as_ref() == Some(qc), - label.clone(), - ) + .selectable_label(selected_contract.as_ref() == Some(qc), label) .clicked() { *selected_contract = Some(qc.clone()); @@ -763,21 +825,17 @@ pub fn add_contract_chooser_pre_filtered<'a, T>( ui.label("Contract:"); ComboBox::from_id_salt("contract_chooser") .width(220.0) - .selected_text(match selected_contract { - Some(qc) => qc - .alias - .clone() - .unwrap_or_else(|| qc.contract.id().to_string(Encoding::Base58)), - None => "Select Contract…".into(), - }) + .selected_text( + selected_contract + .as_ref() + .map(contract_display_label) + .unwrap_or_else(|| "Select Contract…".into()), + ) .show_ui(ui, |cui| { for qc in filtered_contracts { - let label = qc - .alias - .clone() - .unwrap_or_else(|| qc.contract.id().to_string(Encoding::Base58)); + let label = contract_display_label(qc); if cui - .selectable_label(selected_contract.as_ref() == Some(qc), label.clone()) + .selectable_label(selected_contract.as_ref() == Some(qc), label) .clicked() { *selected_contract = Some(qc.clone()); diff --git a/src/ui/wallets/account_summary.rs b/src/ui/wallets/account_summary.rs index 2167ecb07..a766d7f46 100644 --- a/src/ui/wallets/account_summary.rs +++ b/src/ui/wallets/account_summary.rs @@ -55,7 +55,10 @@ impl AccountCategory { 0 => "Main Account".to_string(), idx => format!("BIP44 Account #{}", idx), }, - AccountCategory::Bip32 => format!("BIP32 Account {:?}", index.unwrap_or(0)), + AccountCategory::Bip32 => match index { + Some(idx) if idx > 0 => format!("Legacy BIP32 Account #{}", idx), + _ => "Legacy BIP32 Account".to_string(), + }, AccountCategory::CoinJoin => "CoinJoin".to_string(), AccountCategory::IdentityRegistration => "Identity Registration".to_string(), AccountCategory::IdentitySystem => "Identity System".to_string(), @@ -93,9 +96,9 @@ impl AccountCategory { AccountCategory::Bip44 => { Some("Standard BIP44 account (m/44'/5'/… ) used for normal wallet funds.") } - AccountCategory::Bip32 => { - Some("Legacy BIP32 branch reserved for custom derivations or advanced tools.") - } + AccountCategory::Bip32 => Some( + "Legacy BIP32 account (m/0'/… ). Funds here were received on older derivation paths.", + ), AccountCategory::CoinJoin => { Some("CoinJoin mixing account. Funds here are earmarked for privacy transactions.") } diff --git a/src/ui/wallets/wallets_screen/dialogs.rs b/src/ui/wallets/wallets_screen/dialogs.rs index 43cd911b8..23a71f366 100644 --- a/src/ui/wallets/wallets_screen/dialogs.rs +++ b/src/ui/wallets/wallets_screen/dialogs.rs @@ -207,6 +207,24 @@ impl WalletsBalancesScreen { return AppAction::None; } + // Refresh cached balances from the wallet so SPV updates are reflected + if let Some(wallet) = &self.selected_wallet + && let Ok(wallet_guard) = wallet.read() + { + use dash_sdk::dashcore_rpc::dashcore::address::NetworkUnchecked; + for (addr_str, balance) in &mut self.receive_dialog.core_addresses { + if let Ok(addr) = addr_str.parse::>() + && let Ok(addr) = addr.require_network(self.app_context.network) + { + *balance = wallet_guard + .address_balances + .get(&addr) + .copied() + .unwrap_or(0); + } + } + } + let dark_mode = ctx.style().visuals.dark_mode; // Determine current address based on selected type