diff --git a/AGENTS.md b/AGENTS.md index 07f0babe7..207af94d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,7 +128,7 @@ User-facing error messages (shown in `MessageBanner` via `Display`) must follow 2. **Structure**: *What happened* + *what to do*. Every message must include a concrete action the user can take themselves: retry, wait, try a different approach. Never redirect to "contact support" — users must be able to self-resolve. 3. **Tone**: Calm, direct, brief. Not apologetic ("Sorry!"), not alarming ("Something went wrong!"), not vague ("An error occurred"). 4. **Technical details**: Never in the message itself — no raw error strings, stack traces, SDK internals, or error codes. Attach via `BannerHandle::with_details(e)` — the `Debug` repr goes to the collapsible details panel and logs. Never refer users to "details" or "details panel" — these are not visible in basic mode. Exception: Base58 identifiers (see rule 6) are not technical details — they are user-meaningful handles. -5. **Reference implementation**: `sdk_error_user_message()` in `src/backend_task/error.rs` demonstrates the pattern for SDK errors. New `TaskError` variants should follow the same style. +5. **Reference implementation**: `impl From for TaskError` in `src/backend_task/error.rs` demonstrates the pattern for SDK errors. New `TaskError` variants should follow the same style. 6. **Base58 IDs are allowed in messages**: Contract IDs, identity IDs, document IDs, and similar Base58-encoded identifiers may appear in user-facing messages when they help the user identify which object is involved (e.g., *"This key conflicts with an existing key bound to contract `Abc123…`."*). They are not jargon — they are opaque-but-copyable handles the user can look up. 7. **Use dedicated `TaskError` variants**: Every error should get a dedicated `TaskError` variant with a `#[source]` field that preserves the error chain, enables structural matching, and keeps `Display` / `Debug` separation explicit. For `#[source]` fields in SDK-originated error variants, use `Box` — convert upstream types (e.g. `ProtocolError`) via `SdkError::Protocol(e)`. Use the concrete domain type directly for non-SDK errors (e.g. `rusqlite::Error`). Omit `#[source]` entirely when the upstream error carries no useful diagnostic information (e.g. a channel `SendError`). **Never store user-facing strings in error variants** — error variants must not contain `String` fields that hold messages for the user. The `#[error("...")]` attribute on the variant provides the user-facing message; `String` fields (regardless of name) break this separation. Instead, create a dedicated variant with typed `#[source]` fields, or a fieldless variant if no upstream error exists. When encountering existing variants that store stringified errors, replace them with properly typed variants as part of the change. diff --git a/Cargo.lock b/Cargo.lock index 00d31d4ed..ba8eb6fd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,7 +146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common 0.1.7", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -179,6 +179,7 @@ dependencies = [ "cipher 0.5.2", "cpubits", "cpufeatures 0.3.1", + "zeroize", ] [[package]] @@ -233,6 +234,18 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ambassador" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3d4da7fddbc6567cbd7b5c30364f91313701b1857ab5b925eb3acf06162eed" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "android-activity" version = "0.6.1" @@ -309,7 +322,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -320,7 +333,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -329,6 +342,17 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "app-connect-contract" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" +dependencies = [ + "platform-value", + "platform-version", + "serde_json", + "thiserror 2.0.20", +] + [[package]] name = "apple-native-keyring-store" version = "1.0.0" @@ -770,11 +794,21 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base58ck" -version = "0.1.101" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "365c0acd5b2e8dd0111a46c4faea83fb3cfb6e39a49a7c73a06e090db7b2eff0" +checksum = "df540278d807651653f9b6f21e18903d790d7769eadfbc0dc4df0a141260318f" dependencies = [ - "bitcoin_hashes", + "bitcoin-internals 0.6.0", + "bitcoin_hashes 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base58ck" +version = "0.5.0" +source = "git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516#7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516" +dependencies = [ + "bitcoin-internals 0.7.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "bitcoin_hashes 1.2.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", ] [[package]] @@ -795,15 +829,6 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" -[[package]] -name = "base64-compat" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" -dependencies = [ - "byteorder", -] - [[package]] name = "base64ct" version = "1.8.3" @@ -896,7 +921,7 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.14.101", "rand 0.8.8", "rand_core 0.6.4", "serde", @@ -937,27 +962,130 @@ checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" [[package]] name = "bitcoin-consensus-encoding" version = "1.2.0" +source = "git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516#7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516" +dependencies = [ + "bitcoin-internals 0.7.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "hex-conservative 1.3.0", + "serde", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6712f9c6fd6785b3b270884e57c441c403dc5d7e19ca45368c97c7a1de3000ec" +checksum = "9daa31138eb443d5751b207f3f64154e2bb09cd59960562ccc7a7112be38147f" dependencies = [ - "bitcoin-internals", + "bitcoin-internals 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "hex-conservative 1.3.0", "serde", ] +[[package]] +name = "bitcoin-crypto" +version = "0.3.0" +source = "git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516#7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516" +dependencies = [ + "base58ck 0.5.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "bitcoin-internals 0.7.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "bitcoin-network-kind", + "bitcoin-primitives 0.103.1", + "bitcoin_hashes 1.2.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "hex-conservative 1.3.0", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" +dependencies = [ + "hex-conservative 0.3.2", +] + [[package]] name = "bitcoin-internals" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" +[[package]] +name = "bitcoin-internals" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8bea3a9f0cfece4564e37cb49a38cc245ca5184719e50d7d0dda3268722c4e2" + +[[package]] +name = "bitcoin-internals" +version = "0.7.0" +source = "git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516#7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516" +dependencies = [ + "serde", +] + [[package]] name = "bitcoin-io" version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" dependencies = [ - "bitcoin-consensus-encoding", + "bitcoin-consensus-encoding 1.3.0", +] + +[[package]] +name = "bitcoin-network-kind" +version = "1.0.0" +source = "git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516#7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516" +dependencies = [ + "bitcoin-internals 0.7.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "serde", +] + +[[package]] +name = "bitcoin-primitives" +version = "0.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e12cb1814d037edbd667554c2c0e6b067db72c83ee935b645a6b5829aca908" +dependencies = [ + "bitcoin-consensus-encoding 1.3.0", + "bitcoin-internals 0.6.0", + "bitcoin-units 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitcoin_hashes 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hex-conservative 1.3.0", +] + +[[package]] +name = "bitcoin-primitives" +version = "0.103.1" +source = "git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516#7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516" +dependencies = [ + "bitcoin-consensus-encoding 1.2.0", + "bitcoin-internals 0.7.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "bitcoin-units 0.5.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "bitcoin_hashes 1.2.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "hex-conservative 1.3.0", +] + +[[package]] +name = "bitcoin-units" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae6dcb008158c2bdbe698470280439844be5f640310e4f5d6ef0a0209e066c2" +dependencies = [ + "bitcoin-consensus-encoding 1.3.0", + "bitcoin-internals 0.5.0", + "serde", +] + +[[package]] +name = "bitcoin-units" +version = "0.5.0" +source = "git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516#7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516" +dependencies = [ + "bitcoin-consensus-encoding 1.2.0", + "bitcoin-internals 0.7.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "serde", ] [[package]] @@ -968,6 +1096,29 @@ checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ "bitcoin-io", "hex-conservative 0.2.3", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e" +dependencies = [ + "bitcoin-consensus-encoding 1.3.0", + "bitcoin-internals 0.6.0", + "hex-conservative 1.3.0", +] + +[[package]] +name = "bitcoin_hashes" +version = "1.2.0" +source = "git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516#7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516" +dependencies = [ + "bitcoin-consensus-encoding 1.2.0", + "bitcoin-internals 0.7.0 (git+https://github.com/rust-bitcoin/rust-bitcoin?rev=7ba35c7c1dd63892bfb9c9395bbd80e2ab5df516)", + "hex-conservative 1.3.0", + "serde", ] [[package]] @@ -1044,7 +1195,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1062,7 +1213,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1111,41 +1262,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] -[[package]] -name = "blsful" -version = "3.0.0" -source = "git+https://github.com/dashpay/agora-blsful?rev=0c34a7a488a0bd1c9a9a2196e793b303ad35c900#0c34a7a488a0bd1c9a9a2196e793b303ad35c900" -dependencies = [ - "anyhow", - "blstrs_plus", - "hex", - "hkdf", - "merlin", - "pairing", - "rand 0.8.8", - "rand_chacha", - "rand_core 0.6.4", - "serde", - "serde_bare", - "sha2 0.10.9", - "sha3", - "subtle", - "thiserror 2.0.20", - "uint-zigzag", - "vsss-rs", - "zeroize", -] - [[package]] name = "blst" -version = "0.3.12" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62dc83a094a71d43eeadd254b1ec2d24cb6a0bb6cadce00df51f0db594711a32" +checksum = "c20659f9bbee16cbbd2f7393e40ab6309f5a98f76a2eb57a995ec508b72387fe" dependencies = [ "cc", "glob", @@ -1153,24 +1279,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "blstrs_plus" -version = "0.8.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a16dd4b0d6b4538e1fa0388843acb186363082713a8fc8416d802a04d013818" -dependencies = [ - "arrayref", - "blst", - "elliptic-curve", - "ff", - "group", - "pairing", - "rand_core 0.6.4", - "serde", - "subtle", - "zeroize", -] - [[package]] name = "bs58" version = "0.5.1" @@ -1845,26 +1953,13 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", - "serdect", - "subtle", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", "typenum", ] @@ -1931,6 +2026,7 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.11.3", "fiat-crypto 0.3.0", + "rand_core 0.10.1", "rustc_version", "subtle", "zeroize", @@ -1949,14 +2045,15 @@ dependencies = [ [[package]] name = "dapi-grpc" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "dash-platform-macros", "futures-core", "getrandom 0.2.17", "platform-version", "prost", + "prost-build", "serde", "serde_bytes", "serde_json", @@ -2054,8 +2151,8 @@ dependencies = [ [[package]] name = "dash-async" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "futures", "thiserror 2.0.20", @@ -2066,8 +2163,8 @@ dependencies = [ [[package]] name = "dash-context-provider" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "dash-async", "dpp", @@ -2160,8 +2257,8 @@ dependencies = [ [[package]] name = "dash-network" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" dependencies = [ "cbindgen", "grovedb-bincode", @@ -2171,16 +2268,86 @@ dependencies = [ [[package]] name = "dash-network-seeds" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" dependencies = [ "dash-network", ] +[[package]] +name = "dash-num" +version = "0.1.0-beta" +source = "git+https://github.com/dashpay/base-sdk?rev=33fcc59e1868c803f9608e0e78ee8bddf3abcee7#33fcc59e1868c803f9608e0e78ee8bddf3abcee7" +dependencies = [ + "ambassador", + "bitcoin-internals 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dash-types 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=33fcc59e1868c803f9608e0e78ee8bddf3abcee7)", + "delegate", + "hex-conservative 1.3.0", +] + +[[package]] +name = "dash-num" +version = "0.1.0-beta" +source = "git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5#d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5" +dependencies = [ + "ambassador", + "bitcoin-internals 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dash-types 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5)", + "delegate", + "hex-conservative 1.3.0", + "serde", +] + +[[package]] +name = "dash-pkc" +version = "0.1.0-beta" +source = "git+https://github.com/dashpay/base-sdk?rev=33fcc59e1868c803f9608e0e78ee8bddf3abcee7#33fcc59e1868c803f9608e0e78ee8bddf3abcee7" +dependencies = [ + "aes 0.9.3", + "bitcoin_hashes 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "blst", + "cfg-if", + "dash-num 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=33fcc59e1868c803f9608e0e78ee8bddf3abcee7)", + "dash-types 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=33fcc59e1868c803f9608e0e78ee8bddf3abcee7)", + "ff 0.14.0", + "group 0.14.0", + "hex-conservative 1.3.0", + "rand_core 0.10.1", + "sha2 0.11.0", + "subtle", + "zeroize", +] + +[[package]] +name = "dash-pkc" +version = "0.1.0-beta" +source = "git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5#d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5" +dependencies = [ + "aes 0.9.3", + "base58ck 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitcoin_hashes 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "blst", + "cfg-if", + "dash-num 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5)", + "dash-types 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5)", + "ed25519-dalek 3.0.0", + "ff 0.14.0", + "group 0.14.0", + "hex-conservative 1.3.0", + "rand_core 0.10.1", + "secp256k1", + "serde", + "sha2 0.11.0", + "subtle", + "thiserror 2.0.20", + "zeroize", +] + [[package]] name = "dash-platform-macros" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "heck", "quote", @@ -2189,8 +2356,8 @@ dependencies = [ [[package]] name = "dash-platform-queries" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "dapi-grpc", "dash-context-provider", @@ -2207,8 +2374,8 @@ dependencies = [ [[package]] name = "dash-sdk" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "arc-swap", "async-trait", @@ -2246,8 +2413,8 @@ dependencies = [ [[package]] name = "dash-spv" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" dependencies = [ "async-trait", "chrono", @@ -2257,10 +2424,10 @@ dependencies = [ "dashcore_hashes", "futures", "git-state", - "hex", + "hex-conservative 1.3.0", "key-wallet", "key-wallet-manager", - "rand 0.8.8", + "rand 0.9.5", "rayon", "serde", "serde_json", @@ -2273,44 +2440,90 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "dash-types" +version = "0.1.0-beta" +source = "git+https://github.com/dashpay/base-sdk?rev=33fcc59e1868c803f9608e0e78ee8bddf3abcee7#33fcc59e1868c803f9608e0e78ee8bddf3abcee7" +dependencies = [ + "cfg-if", + "hex-conservative 1.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "dash-types" +version = "0.1.0-beta" +source = "git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5#d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5" +dependencies = [ + "bitcoin-consensus-encoding 1.3.0", + "bitcoin-primitives 0.103.0", + "cfg-if", + "hex-conservative 1.3.0", + "serde", + "subtle", + "thiserror 2.0.20", + "zeroize", +] + [[package]] name = "dashcore" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" dependencies = [ "anyhow", - "base64-compat", + "base64 0.22.1", "bech32 0.9.1", "bitvec", "blake3", - "blsful", "dash-network", + "dash-num 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5)", + "dash-types 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5)", + "dashcore-crypto", "dashcore-private", "dashcore_hashes", - "ed25519-dalek 2.2.0", "grovedb-bincode", "grovedb-bincode-derive", - "hex", - "hex_lit", - "rustversion", - "secp256k1", + "hex-conservative 1.3.0", "serde", "thiserror 2.0.20", "tracing", ] +[[package]] +name = "dashcore-crypto" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" +dependencies = [ + "base58ck 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitcoin-crypto", + "dash-network", + "dash-pkc 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5)", + "dash-types 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5)", + "dashcore-private", + "dashcore_hashes", + "delegate", + "grovedb-bincode", + "grovedb-bincode-derive", + "hex-conservative 1.3.0", + "serde", + "thiserror 2.0.20", + "tracing", + "zeroize", +] + [[package]] name = "dashcore-private" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" [[package]] name = "dashcore-rpc" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" dependencies = [ "dashcore-rpc-json", - "hex", + "hex-conservative 1.3.0", "jsonrpc", "serde", "serde_json", @@ -2319,12 +2532,12 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" dependencies = [ "dashcore", "grovedb-bincode", - "hex", + "hex-conservative 1.3.0", "key-wallet", "serde", "serde_json", @@ -2334,19 +2547,20 @@ dependencies = [ [[package]] name = "dashcore_hashes" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" dependencies = [ - "dashcore-private", + "bitcoin_hashes 0.14.101", "grovedb-bincode", + "hex-conservative 1.3.0", "rs-x11-hash", "serde", ] [[package]] name = "dashpay-contract" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "platform-value", "platform-version", @@ -2356,14 +2570,16 @@ dependencies = [ [[package]] name = "data-contracts" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ + "app-connect-contract", "dashpay-contract", "document-history-contract", "dpns-contract", "keyword-search-contract", "masternode-reward-shares-contract", + "moderation-charters-contract", "platform-value", "platform-version", "serde_json", @@ -2450,6 +2666,17 @@ dependencies = [ "thiserror 2.0.20", ] +[[package]] +name = "delegate" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "der" version = "0.7.10" @@ -2606,7 +2833,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2658,8 +2885,8 @@ dependencies = [ [[package]] name = "document-history-contract" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "platform-value", "platform-version", @@ -2687,8 +2914,8 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "dpns-contract" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "platform-value", "platform-version", @@ -2698,26 +2925,31 @@ dependencies = [ [[package]] name = "dpp" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "anyhow", "async-trait", "base64 0.22.1", "bech32 0.11.1", + "blst", "bs58", "byteorder", "chrono", "chrono-tz", "ciborium", + "dash-pkc 0.1.0-beta (git+https://github.com/dashpay/base-sdk?rev=33fcc59e1868c803f9608e0e78ee8bddf3abcee7)", "dash-spv", "dashcore", "dashcore-rpc", "data-contracts", "derive_more 1.0.0", "dpp-json-convertible-derive", + "ed25519-dalek 2.2.0", "env_logger", "getrandom 0.2.17", + "getrandom 0.3.4", + "getrandom 0.4.3", "grovedb-bincode", "grovedb-commitment-tree", "hex", @@ -2738,6 +2970,7 @@ dependencies = [ "platform-versioning", "rand 0.8.8", "regex", + "secp256k1", "serde", "serde_json", "serde_repr", @@ -2745,12 +2978,13 @@ dependencies = [ "strum", "thiserror 2.0.20", "tracing", + "zeroize", ] [[package]] name = "dpp-json-convertible-derive" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "proc-macro2", "quote", @@ -2759,19 +2993,20 @@ dependencies = [ [[package]] name = "drive" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "byteorder", "derive_more 1.0.0", "dpp", - "grovedb 6.0.1", + "grovedb 6.1.1", "grovedb-bincode", - "grovedb-costs 6.0.1", + "grovedb-costs 6.1.1", "grovedb-epoch-based-storage-flags", - "grovedb-path 6.0.1", + "grovedb-merk 6.1.1", + "grovedb-path 6.1.1", "grovedb-query", - "grovedb-version 6.0.1", + "grovedb-version 6.1.1", "hex", "indexmap 2.14.2", "integer-encoding", @@ -2785,8 +3020,8 @@ dependencies = [ [[package]] name = "drive-proof-verifier" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "dapi-grpc", "dash-context-provider", @@ -2892,7 +3127,9 @@ checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek 5.0.0", "ed25519 3.0.0", + "rand_core 0.10.1", "sha2 0.11.0", + "signature 3.0.0", "subtle", "zeroize", ] @@ -3072,41 +3309,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array 0.14.7", - "group", - "hkdf", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "tap", - "zeroize", -] - -[[package]] -name = "elliptic-curve-tools" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de2b6fae800f08032a6ea32995b52925b1d451bff9d445c8ab2932323277faf" -dependencies = [ - "elliptic-curve", - "heapless 0.8.0", - "hex", - "multiexp", - "serde", - "zeroize", -] - [[package]] name = "emath" version = "0.36.2" @@ -3305,7 +3507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3415,6 +3617,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -3733,18 +3945,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", -] - -[[package]] -name = "generic-array" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" -dependencies = [ - "rustversion", - "serde_core", - "typenum", ] [[package]] @@ -3777,9 +3977,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -3828,8 +4030,8 @@ dependencies = [ [[package]] name = "git-state" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" [[package]] name = "gl_generator" @@ -3974,11 +4176,20 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "memuse", - "rand 0.8.8", "rand_core 0.6.4", - "rand_xorshift", + "subtle", +] + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", "subtle", ] @@ -4006,21 +4217,21 @@ dependencies = [ [[package]] name = "grovedb" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "blake3", "grovedb-bincode", "grovedb-bincode-derive", "grovedb-bulk-append-tree", - "grovedb-costs 6.0.1", + "grovedb-costs 6.1.1", "grovedb-dense-fixed-sized-merkle-tree", - "grovedb-element 6.0.1", - "grovedb-merk 6.0.1", + "grovedb-element 6.1.1", + "grovedb-merk 6.1.1", "grovedb-merkle-mountain-range", - "grovedb-path 6.0.1", + "grovedb-path 6.1.1", "grovedb-query", - "grovedb-version 6.0.1", + "grovedb-version 6.1.1", "hex", "indexmap 2.14.2", "integer-encoding", @@ -4049,16 +4260,16 @@ dependencies = [ [[package]] name = "grovedb-bulk-append-tree" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "blake3", "grovedb-bincode", - "grovedb-costs 6.0.1", + "grovedb-costs 6.1.1", "grovedb-dense-fixed-sized-merkle-tree", "grovedb-merkle-mountain-range", "grovedb-query", - "grovedb-version 6.0.1", + "grovedb-version 6.1.1", "hex", "integer-encoding", "thiserror 2.0.20", @@ -4066,13 +4277,13 @@ dependencies = [ [[package]] name = "grovedb-commitment-tree" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "blake3", "grovedb-bulk-append-tree", - "grovedb-costs 6.0.1", - "grovedb-version 6.0.1", + "grovedb-costs 6.1.1", + "grovedb-version 6.1.1", "incrementalmerkletree", "orchard", "rusqlite", @@ -4092,8 +4303,8 @@ dependencies = [ [[package]] name = "grovedb-costs" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "integer-encoding", "intmap", @@ -4102,14 +4313,14 @@ dependencies = [ [[package]] name = "grovedb-dense-fixed-sized-merkle-tree" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "blake3", "grovedb-bincode", - "grovedb-costs 6.0.1", + "grovedb-costs 6.1.1", "grovedb-query", - "grovedb-version 6.0.1", + "grovedb-version 6.1.1", "thiserror 2.0.20", ] @@ -4129,14 +4340,14 @@ dependencies = [ [[package]] name = "grovedb-element" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "grovedb-bincode", "grovedb-bincode-derive", - "grovedb-path 6.0.1", + "grovedb-path 6.1.1", "grovedb-query", - "grovedb-version 6.0.1", + "grovedb-version 6.1.1", "hex", "integer-encoding", "thiserror 2.0.20", @@ -4144,10 +4355,10 @@ dependencies = [ [[package]] name = "grovedb-epoch-based-storage-flags" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ - "grovedb-costs 6.0.1", + "grovedb-costs 6.1.1", "hex", "integer-encoding", "intmap", @@ -4177,20 +4388,20 @@ dependencies = [ [[package]] name = "grovedb-merk" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "blake3", "byteorder", "ed", "grovedb-bincode", "grovedb-bincode-derive", - "grovedb-costs 6.0.1", - "grovedb-element 6.0.1", - "grovedb-path 6.0.1", + "grovedb-costs 6.1.1", + "grovedb-element 6.1.1", + "grovedb-path 6.1.1", "grovedb-query", - "grovedb-version 6.0.1", - "grovedb-visualize 6.0.1", + "grovedb-version 6.1.1", + "grovedb-visualize 6.1.1", "hex", "indexmap 2.14.2", "integer-encoding", @@ -4199,13 +4410,13 @@ dependencies = [ [[package]] name = "grovedb-merkle-mountain-range" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "blake3", "grovedb-bincode", - "grovedb-costs 6.0.1", - "grovedb-version 6.0.1", + "grovedb-costs 6.1.1", + "grovedb-version 6.1.1", ] [[package]] @@ -4218,16 +4429,16 @@ dependencies = [ [[package]] name = "grovedb-path" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "hex", ] [[package]] name = "grovedb-query" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "byteorder", "ed", @@ -4249,8 +4460,8 @@ dependencies = [ [[package]] name = "grovedb-version" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "thiserror 2.0.20", "versioned-feature-core", @@ -4267,8 +4478,8 @@ dependencies = [ [[package]] name = "grovedb-visualize" -version = "6.0.1" -source = "git+https://github.com/dashpay/grovedb?rev=74818ceb95805f56f141d24ca41f58b75c031a7f#74818ceb95805f56f141d24ca41f58b75c031a7f" +version = "6.1.1" +source = "git+https://github.com/dashpay/grovedb?rev=2128ea18f9b1c2f9502f99967b1b1c6485c91f8b#2128ea18f9b1c2f9502f99967b1b1c6485c91f8b" dependencies = [ "hex", "itertools 0.14.0", @@ -4359,8 +4570,8 @@ checksum = "fb2a697cad929f706b7987fe804ad57d43622cd37463ba7e4d662a926fdcfea3" dependencies = [ "arrayvec", "bitvec", - "ff", - "group", + "ff 0.13.1", + "group 0.13.0", "halo2_poseidon", "halo2_proofs", "lazy_static", @@ -4384,8 +4595,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa3da60b81f02f9b33ebc6252d766f843291fb4d2247a07ae73d20b791fc56f" dependencies = [ "bitvec", - "ff", - "group", + "ff 0.13.1", + "group 0.13.0", "pasta_curves", ] @@ -4396,8 +4607,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5aca1c66059a919227dec97444a11a4350d2f9c820ca48690988f0aa0e81cbf" dependencies = [ "blake2b_simd", - "ff", - "group", + "ff 0.13.1", + "group 0.13.0", "halo2_legacy_pdqsort", "indexmap 1.9.3", "maybe-rayon", @@ -4427,15 +4638,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -4487,20 +4689,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", - "hash32 0.2.1", + "hash32", "rustc_version", "serde", - "spin 0.9.9", - "stable_deref_trait", -] - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32 0.3.1", + "spin", "stable_deref_trait", ] @@ -4521,9 +4713,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hex-conservative" @@ -4534,6 +4723,15 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-conservative" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex-conservative" version = "1.3.0" @@ -4549,12 +4747,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" -[[package]] -name = "hex_lit" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" - [[package]] name = "hkdf" version = "0.12.4" @@ -4951,7 +5143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding 0.3.3", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -5168,8 +5360,8 @@ checksum = "8499f7a74008aafbecb2a2e608a3e13e4dd3e84df198b604451efe93f2de6e61" dependencies = [ "bitvec", "bls12_381", - "ff", - "group", + "ff 0.13.1", + "group 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -5185,22 +5377,19 @@ dependencies = [ [[package]] name = "key-wallet" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" dependencies = [ "async-trait", - "base58ck", "bip39", "bitflags 2.13.2", "dashcore", "dashcore-private", "dashcore_hashes", - "getrandom 0.2.17", - "hex", - "secp256k1", + "getrandom 0.4.3", + "hex-conservative 1.3.0", "serde", "serde_json", - "sha2 0.10.9", "tracing", "unicode-normalization", "zeroize", @@ -5208,11 +5397,12 @@ dependencies = [ [[package]] name = "key-wallet-manager" -version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=e4208c90786a6854bd498315bcb571ef24182c15#e4208c90786a6854bd498315bcb571ef24182c15" +version = "0.46.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=aca020263990a86a15cc1d97ae2cbf965d488db5#aca020263990a86a15cc1d97ae2cbf965d488db5" dependencies = [ "async-trait", "dashcore", + "hex-conservative 1.3.0", "key-wallet", "rayon", "serde", @@ -5232,8 +5422,8 @@ dependencies = [ [[package]] name = "keyword-search-contract" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "platform-value", "platform-version", @@ -5286,7 +5476,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.9.9", + "spin", ] [[package]] @@ -5427,8 +5617,8 @@ dependencies = [ [[package]] name = "masternode-reward-shares-contract" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "platform-value", "platform-version", @@ -5579,6 +5769,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moderation-charters-contract" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" +dependencies = [ + "platform-value", + "platform-version", + "serde_json", + "thiserror 2.0.20", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -5589,20 +5790,6 @@ dependencies = [ "pxfm", ] -[[package]] -name = "multiexp" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" -dependencies = [ - "ff", - "group", - "rand_core 0.6.4", - "rustversion", - "std-shims", - "zeroize", -] - [[package]] name = "multimap" version = "0.10.1" @@ -5799,7 +5986,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5824,8 +6011,6 @@ checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", - "rand 0.8.8", - "serde", ] [[package]] @@ -5835,8 +6020,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", - "rand 0.8.8", - "serde", ] [[package]] @@ -5884,7 +6067,6 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", - "serde", ] [[package]] @@ -6363,10 +6545,10 @@ dependencies = [ "bitvec", "blake2b_simd", "corez", - "ff", + "ff 0.13.1", "fpe", "getset", - "group", + "group 0.13.0", "halo2_gadgets", "halo2_poseidon", "halo2_proofs", @@ -6427,15 +6609,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "pairing" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" -dependencies = [ - "group", -] - [[package]] name = "parking" version = "2.2.1" @@ -6502,8 +6675,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3437083215c505e867eea5478371feba43d7689d6d15ec0a209eb46fb0d4cda6" dependencies = [ "blake2b_simd", - "ff", - "group", + "ff 0.13.1", + "group 0.13.0", "lazy_static", "rand 0.8.8", "static_assertions", @@ -6733,8 +6906,8 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "platform-encryption" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "aes 0.8.4", "cbc 0.1.2", @@ -6746,8 +6919,8 @@ dependencies = [ [[package]] name = "platform-serialization" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "grovedb-bincode", "platform-version", @@ -6755,8 +6928,8 @@ dependencies = [ [[package]] name = "platform-serialization-derive" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "proc-macro2", "quote", @@ -6766,8 +6939,8 @@ dependencies = [ [[package]] name = "platform-value" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "base64 0.22.1", "bs58", @@ -6786,19 +6959,19 @@ dependencies = [ [[package]] name = "platform-version" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "grovedb-bincode", - "grovedb-version 6.0.1", + "grovedb-version 6.1.1", "thiserror 2.0.20", "versioned-feature-core", ] [[package]] name = "platform-versioning" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "proc-macro2", "quote", @@ -6807,8 +6980,8 @@ dependencies = [ [[package]] name = "platform-wallet" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "arc-swap", "async-trait", @@ -6841,8 +7014,8 @@ dependencies = [ [[package]] name = "platform-wallet-storage" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "apple-native-keyring-store", "argon2 0.5.3", @@ -7002,7 +7175,7 @@ dependencies = [ "cobs", "embedded-io 0.4.0", "embedded-io 0.6.1", - "heapless 0.7.17", + "heapless", "serde", ] @@ -7097,7 +7270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", - "itertools 0.14.0", + "itertools 0.10.5", "log", "multimap", "petgraph", @@ -7118,7 +7291,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.119", @@ -7237,7 +7410,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7274,10 +7447,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.2" @@ -7299,6 +7482,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -7308,6 +7501,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.1" @@ -7323,15 +7525,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_xorshift" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "range-alloc" version = "0.1.5" @@ -7405,7 +7598,7 @@ dependencies = [ "blake2b_simd", "byteorder", "frost-rerandomized", - "group", + "group 0.13.0", "hex", "jubjub", "pasta_curves", @@ -7795,8 +7988,8 @@ dependencies = [ [[package]] name = "rs-dapi-client" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "backon", "chrono", @@ -7807,6 +8000,7 @@ dependencies = [ "hex", "http", "http-serde", + "hyper-util", "lru", "rand 0.8.8", "serde", @@ -7814,29 +8008,35 @@ dependencies = [ "sha2 0.10.9", "thiserror 2.0.20", "tokio", + "tokio-socks", "tonic-web-wasm-client", + "tower-service", "tracing", "wasm-bindgen-futures", ] [[package]] name = "rs-sdk-trusted-context-provider" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "arc-swap", "dash-async", "dash-context-provider", "dpp", "futures", + "gloo-timers", "hex", "lru", "reqwest 0.12.28", "serde", "serde_json", "thiserror 2.0.20", + "tokio", "tracing", "url", + "wasm-bindgen-futures", + "web-time", ] [[package]] @@ -7954,7 +8154,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8013,7 +8213,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8115,37 +8315,22 @@ dependencies = [ "tiny-skia 0.11.4", ] -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array 0.14.7", - "pkcs8", - "subtle", - "zeroize", -] - [[package]] name = "secp256k1" -version = "0.30.0" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +checksum = "d7f404a8dab7a7a5a631e741d8699aa9c8e1d689fc26ccf897c7187565490b69" dependencies = [ - "bitcoin_hashes", - "rand 0.8.8", + "rand 0.9.5", "secp256k1-sys", "serde", ] [[package]] name = "secp256k1-sys" -version = "0.10.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +checksum = "f6b2992d4a3cd244539a7d5d0966aadbe5ca7fb868a5d7e38c29499b9e709bbd" dependencies = [ "cc", ] @@ -8195,15 +8380,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde_bare" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c55386eed0f1ae957b091dc2ca8122f287b60c79c774cbe3d5f2b69fded660" -dependencies = [ - "serde", -] - [[package]] name = "serde_bytes" version = "0.11.19" @@ -8442,6 +8618,9 @@ name = "signature" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "rand_core 0.10.1", +] [[package]] name = "simd-adler32" @@ -8480,7 +8659,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d268ae0ea06faafe1662e9967cd4f9022014f5eeb798e0c302c876df8b7af9c" dependencies = [ - "group", + "group 0.13.0", "pasta_curves", "subtle", ] @@ -8604,7 +8783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8616,12 +8795,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spin" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" - [[package]] name = "spirv" version = "0.4.0+sdk-1.4.341.0" @@ -8687,17 +8860,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "std-shims" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227c4f8561598188d0df96dbe749824576174bba278b5b6bb2eacff1066067d0" -dependencies = [ - "hashbrown 0.16.1", - "rustversion", - "spin 0.10.1", -] - [[package]] name = "strict-num" version = "0.1.1" @@ -8853,10 +9015,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -9096,8 +9258,8 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "token-history-contract" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "platform-value", "platform-version", @@ -9175,6 +9337,18 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.19" @@ -9577,7 +9751,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -9592,12 +9766,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "uint-zigzag" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61faa33dc26b2851a37da5390a1a4cac015887b1e97ecd77ce7b4f987431de9f" - [[package]] name = "unicase" version = "2.9.0" @@ -9888,24 +10056,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "vsss-rs" -version = "5.1.0" -source = "git+https://github.com/dashpay/vsss-rs?branch=main#668f1406bf25a4b9a95cd97c9069f7a1632897c3" -dependencies = [ - "crypto-bigint", - "elliptic-curve", - "elliptic-curve-tools", - "generic-array 1.4.5", - "hex", - "num", - "rand_core 0.6.4", - "serde", - "sha3", - "subtle", - "zeroize", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -9918,8 +10068,8 @@ dependencies = [ [[package]] name = "wallet-utils-contract" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "platform-value", "platform-version", @@ -10475,7 +10625,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10932,7 +11082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11061,8 +11211,8 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "withdrawals-contract" -version = "4.2.0-beta.3" -source = "git+https://github.com/dashpay/platform?rev=9f7ed16935bd540e4c2a542688800d2953f9f67a#9f7ed16935bd540e4c2a542688800d2953f9f67a" +version = "5.0.0-rc.8" +source = "git+https://github.com/dashpay/platform?rev=132ae31a1356d035cd8326f27c1109685090a71d#132ae31a1356d035cd8326f27c1109685090a71d" dependencies = [ "num_enum 0.5.11", "platform-value", @@ -11350,9 +11500,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "serde", "zeroize_derive", diff --git a/Cargo.toml b/Cargo.toml index bd1648d5e..1460a7d7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ base64 = "0.23.1" # and `DisplayFromStr` adapters — the vulnerable `KeyValueMap` serializer is never invoked in the # dependency chain, so the advisory is not exploitable here (Dependabot alert #41; dismissal as # `not_used` was attempted but requires manual GitHub action — see coordinator note). -dash-sdk = { git = "https://github.com/dashpay/platform", rev = "9f7ed16935bd540e4c2a542688800d2953f9f67a", features = [ +dash-sdk = { git = "https://github.com/dashpay/platform", rev = "132ae31a1356d035cd8326f27c1109685090a71d", features = [ "core_key_wallet", "core_key_wallet_manager", "core_bincode", @@ -34,15 +34,15 @@ dash-sdk = { git = "https://github.com/dashpay/platform", rev = "9f7ed16935bd540 "core_spv", "shielded", ] } -rs-sdk-trusted-context-provider = { git = "https://github.com/dashpay/platform", rev = "9f7ed16935bd540e4c2a542688800d2953f9f67a" } -platform-wallet = { git = "https://github.com/dashpay/platform", rev = "9f7ed16935bd540e4c2a542688800d2953f9f67a", features = [ +rs-sdk-trusted-context-provider = { git = "https://github.com/dashpay/platform", rev = "132ae31a1356d035cd8326f27c1109685090a71d" } +platform-wallet = { git = "https://github.com/dashpay/platform", rev = "132ae31a1356d035cd8326f27c1109685090a71d", features = [ "serde", "shielded", ] } # `serde` backs `model::secret::Secret`'s `Deserialize`: the upstream # visitor copies a borrowed `&str` straight into guarded memory instead of # routing through a transient `String`. Secret schemas are included by `secrets`. -platform-wallet-storage = { git = "https://github.com/dashpay/platform", rev = "9f7ed16935bd540e4c2a542688800d2953f9f67a", features = [ +platform-wallet-storage = { git = "https://github.com/dashpay/platform", rev = "132ae31a1356d035cd8326f27c1109685090a71d", features = [ "shielded", "serde", ] } diff --git a/docs/ai-design/2026-10-22-platform-v14-bump/README.md b/docs/ai-design/2026-10-22-platform-v14-bump/README.md new file mode 100644 index 000000000..3617cec67 --- /dev/null +++ b/docs/ai-design/2026-10-22-platform-v14-bump/README.md @@ -0,0 +1,68 @@ +# Platform protocol 14 support (Platform v5.1-dev pin) + +A point-in-time record of PR #1015: DET on Platform `v5.1-dev` +(`132ae31a1356d035cd8326f27c1109685090a71d`, 5.0.0-rc.8) and the protocol 14 features it +adds. It supersedes the open "Devnet protocol seed" item of +[`2026-09-14-platform-4.2-dev-bump`](../2026-09-14-platform-4.2-dev-bump/upgrade-notes.md). + +## Which protocol version DET builds for + +There are two version sources, and they can disagree while a network moves +from 13 to 14: + +- **The SDK's version** (`AppContext::platform_version()`). It starts at + upstream's per-network `dash_sdk::sdk::min_protocol_version` (13 on mainnet, + testnet and regtest, 14 on devnet) and ratchets to what verified responses + report. Transitions are encoded and system contracts cached at this version, + and the cache reloads when it changes. +- **The network's confirmed version** (`AppContext::platform_protocol_version()`). + It comes from a proved epoch read (or a proved DPNS fetch). It is `0` until + confirmed. Feature gates (`context/feature_gate.rs`) read only this one, so + an unconfirmed network never unlocks a protocol 14 feature. + `connected_platform_version()` selects that version's tables for estimates. + +If a protocol 14 structure is encoded at the protocol 13 table, it keeps its +v14 data (a limited key stays `IdentityPublicKeyInCreation::V1`; the +once-per-identity token rules stay V1). A protocol 13 network then refuses it. +It is never silently downgraded (`backend_task/protocol_13_transitions.rs`). + +## Protocol 14 features, all gated on the confirmed version + +| Feature | Where | +|---|---| +| Key spending limits and expiry, raising them | `model/identity_key_limits.rs`, Add Key, Key Info | +| Contract-bound authentication keys | `model/identity_key_usability.rs` | +| Once-per-identity token distributions | `model/token.rs`, token creator, Claim | +| Document action fees, confirmed before signing | `model/fee_estimation.rs`, document action screen | +| Contract fee pots and claims | `backend_task/contract_fee_pots.rs`, Fee Pots screen | +| Failed-withdrawal status | `backend_task/platform_info.rs` | + +## Decisions + +- **Key choice (IDN-024).** Automatic selection keeps only keys that are live, + held on this device, in scope, and at a security level the action accepts. + Among those it prefers unlimited keys. The key is picked again when the + scope changes. The manual chooser leaves out keys bound to another contract + or document type, but keeps expired and limited keys with a warning. +- **What is confirmed is what is sent.** The confirmations for document action + fees and token claims build the action when they open, block the form behind + them, and send exactly that action on confirm. A key limit raise sends the + quoted raise it showed; `check_against` refuses it if the key changed. +- **Only proved data gates an action.** Fee pot claims are not blocked on the + response metadata's epoch, because the proof does not sign it. Platform + refuses a second claim in the same epoch, with a typed error. +- **Raises only.** `KeyLimitsRaise::check_against`, the last check before + signing, refuses a lowered limit, and a limit added to a key that has none. + +## Known blocker: mainnet proofs + +Mainnet nodes still serve the retired protocol 13 proof wire format, which this +SDK rejects. Proof verification stays strict: there is no fallback that skips +or weakens it. The PR stays blocked until an upstream SDK that verifies both +formats, or a server rollout, lands. + +## Existing data + +The upstream V019 wallet-storage migration adds an asset-lock operation id. +V001–V018 are unchanged. The upgrade is forward only: going back needs a copy +taken before the upgrade. diff --git a/docs/user-stories.md b/docs/user-stories.md index ddeba35f3..b512f1b4f 100644 --- a/docs/user-stories.md +++ b/docs/user-stories.md @@ -806,6 +806,39 @@ As a user, I want to remove an identity from this device from the identity's own --- +### IDN-022: Add a key with a spending limit or an expiry [Implemented] +**Persona:** Priya, Jordan + +As a user handing a key to an app or a helper, I want to cap how much that key can ever spend and when it stops working, so that a leaked or forgotten key can do limited damage. + +- The Add Key screen offers "Spending Limit" and "Expires After (days)" for authentication keys below the master level. +- On a network that does not support key limits yet, both options are shown disabled with an explanation. +- Where the network allows it, an authentication key can also be limited to one contract or document type, with or without a spending limit and expiry. +- A zero limit, zero days, or an expiry too far in the future is refused before anything is sent. +- The Key Info screen shows the limit with how much is left of it, and the expiry date; an expired or used-up key is marked. + +### IDN-023: Raise a key's spending limit or extend its expiry [Implemented] +**Persona:** Priya, Jordan + +As a user whose limited key is running out, I want to add to its spending limit or extend its expiry so that I can keep using it without adding a new key. + +- The Key Info screen of a limited key offers "Raise Limits" with an amount to add and a number of days to extend by. +- An expiry is extended from its current date, or from today when the key has already expired. +- An expired key cannot be topped up on its own; the screen also asks to extend its expiry. +- Before anything is sent, a confirmation shows the resulting total spending limit and expiry and the key that signs the change; exactly that is sent. +- If the key's limits, or the key that would sign the change, changed elsewhere in the meantime, nothing is sent and the screen shows the current limits and signing key to review again; no other signing key is picked without a new review. +- The change is signed by the identity's master key, or a critical key without limits; without either on this device, the screen says which key to import. +- After the change, the screen shows the key as the network stores it and its new remaining limit. + +### IDN-024: Signing avoids keys that cannot sign [Implemented] +**Persona:** Alex, Priya + +As a user, I want the app to sign with a key that can actually sign the action, so that my actions are not refused because of an expired, used-up or out-of-scope key. + +- Automatic key choice skips disabled and expired keys, keys limited to other contracts or document types, and keys bound to a contract group. +- Only keys this device holds the private key of are chosen; among those, one without limits is preferred over a limited one. +- Where the user picks the key, limited and expired keys stay selectable, and the key list and selection show a warning explaining the limit. Keys bound to another contract or document type are not offered. + ## DPNS (DPN) ### DPN-001: Register a username [Implemented] @@ -1273,6 +1306,27 @@ As a user, I want to stop tracking a token balance for one of my identities so t --- +### TOK-019: Create a token each identity can claim once [Implemented] +**Persona:** Jordan + +As a token creator, I want every identity to be able to claim a fixed amount of my token exactly once, so that I can run a fair airdrop without a list of recipients. + +- The token creator offers a once-per-identity distribution with the amount each identity can claim, on networks that support it. +- The fee estimate includes the network's extra fee for this distribution. +- The Claim screen and My Tokens show the distribution. +- After a successful claim, the Claim screen notes that the identity may have already claimed and when; the note never blocks claiming and can be dismissed. +- A second claim refused by the network is explained. + +### TOK-020: Create a token holders cannot transfer [Implemented] +**Persona:** Jordan + +As a token creator, I want to create a token that holders cannot send to other identities, so that it can work as a membership badge or a non-transferable credit. + +- The token creator offers "Holders can send this token to other identities", on by default; turning it off is offered only on networks that support it. +- A network that does not support it is refused before anything is paid. +- For a token that cannot be transferred, My Tokens disables Transfer and says why. +- A transfer refused by the network is explained. + ## Contracts and Documents (DOC) ### DOC-001: Register a new data contract [Implemented] @@ -1349,6 +1403,35 @@ As a developer, I want to set a price on a document and allow others to purchase --- +### DOC-010: Agree to a contract's fee for a document action [Implemented] +**Persona:** Alex, Priya + +As a user acting on a document whose contract charges a fee, I want to see that fee and agree to it before anything is sent, so that I am never charged more than I accepted. + +- The estimated-fee panel shows the contract fee next to the network fee, split between the contract owner and its moderators. +- Sending asks for confirmation naming the fee and, for a fee that follows network fees, the most that can be charged ("up to N DASH") and the tolerated increase. +- If network fees rise past that tolerance, the network refuses the action; the app updates the fee, and trying again shows the new amount to confirm. +- If the contract changes its fee after confirmation, nothing is sent and the user is asked to confirm the current fee. + +### DOC-011: Skip an optional token cost [Implemented] +**Persona:** Alex, Priya + +As a user, when a document action's token cost is optional, I want to choose not to pay it, so that I can pay the network fee in credits instead. + +- A "Pay the token cost" option appears only when the token cost is optional. +- Without it, the action carries no token payment and its network fee is paid from the identity's credits. + +### DOC-012: View and claim a contract's fee pots [Implemented] +**Persona:** Jordan + +As a contract owner or moderator, I want to see what my contract's document fees have collected and claim it, so that the fees reach my identity. + +- The Contracts screen offers "Fee Pots" on networks that support contract fees. +- Both pots of a contract are shown with their balance and last payout (epoch, time, claimant). +- Only an identity that receives a pot can claim it; the owner pot goes to the owner, the moderators pot to the whole moderation team. +- A pot can be claimed once per epoch. The app shows the last payout's epoch but does not block a claim on it; a second claim in the same epoch is refused by the network, and the message says to try again once the next epoch starts. +- After a claim, the pot and the claimant's new balance are shown. + ## Developer and Power Tools (DEV) ### DEV-001: Decode state transitions [Implemented] diff --git a/scripts/migration-fixtures/expected-migration-fingerprint.txt b/scripts/migration-fixtures/expected-migration-fingerprint.txt index cdd017629..61d9a0f66 100644 --- a/scripts/migration-fixtures/expected-migration-fingerprint.txt +++ b/scripts/migration-fixtures/expected-migration-fingerprint.txt @@ -5,7 +5,7 @@ # # fingerprint = sha256 of the LC_ALL=C-sorted " " listing of every # file under packages/rs-platform-wallet-storage/migrations at that rev. -pin=9f7ed16935bd540e4c2a542688800d2953f9f67a +pin=132ae31a1356d035cd8326f27c1109685090a71d target_branch=v5.1-dev -migration_count=18 -fingerprint=1c6a683d1b781045f4528048b49b1e945f4da6caeb71e72d8de7429904399b99 +migration_count=19 +fingerprint=79465f56868e4dfd6e0ba05e4eefe7e7d4ac8ef478c2579ba019d12f868278e6 diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index 3a59001e4..b0f03d05e 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -1669,14 +1669,12 @@ mod tests { context.set_det_kv_override_for_test(kv); store.fail_next_gets_containing("det:dpns_vote_operations:v2:", 2); let mut sdk = Sdk::new_mock(); - let document_type = context - .dpns_contract - .document_type_for_name("domain") - .unwrap(); + let dpns_contract = context.dpns_contract(); + let document_type = dpns_contract.document_type_for_name("domain").unwrap(); sdk.mock() .expect_fetch_many::( VotePollsByDocumentTypeQuery { - contract_id: context.dpns_contract.id(), + contract_id: context.dpns_contract().id(), document_type_name: document_type.name().to_owned(), index_name: document_type.find_contested_index().unwrap().name.clone(), start_at_value: None, @@ -2333,14 +2331,12 @@ mod tests { use dash_sdk::query_types::{ContestedResource, ContestedResources}; let (_temp, context) = vote_context(); let mut sdk = Sdk::new_mock(); - let document_type = context - .dpns_contract - .document_type_for_name("domain") - .unwrap(); + let dpns_contract = context.dpns_contract(); + let document_type = dpns_contract.document_type_for_name("domain").unwrap(); sdk.mock() .expect_fetch_many::( VotePollsByDocumentTypeQuery { - contract_id: context.dpns_contract.id(), + contract_id: context.dpns_contract().id(), document_type_name: document_type.name().to_owned(), index_name: document_type.find_contested_index().unwrap().name.clone(), start_at_value: None, @@ -2413,7 +2409,7 @@ mod tests { let votes: ResourceVotesByIdentity = choice .map(|resource_vote_choice| { let vote_poll = VotePoll::ContestedDocumentResourceVotePoll( - crate::model::dpns_voting::dpns_vote_poll(&context.dpns_contract, name) + crate::model::dpns_voting::dpns_vote_poll(&context.dpns_contract(), name) .unwrap(), ); ( diff --git a/src/backend_task/contested_names/query_dpns_contested_resources.rs b/src/backend_task/contested_names/query_dpns_contested_resources.rs index bf64b8b16..fe67eebff 100644 --- a/src/backend_task/contested_names/query_dpns_contested_resources.rs +++ b/src/backend_task/contested_names/query_dpns_contested_resources.rs @@ -153,7 +153,7 @@ impl AppContext { // Held to the end of the pass and taken before any other lock, so a // pass that waits here holds nothing a running pass needs. let _pass = self.dpns_contest_refresh_pass.lock().await; - let data_contract = self.dpns_contract.as_ref(); + let data_contract = self.dpns_contract(); let document_type = data_contract .document_type_for_name("domain") .map_err(|_| TaskError::ContractSchemaMismatch { @@ -456,12 +456,12 @@ mod tests { context.set_det_kv_override_for_test(crate::wallet_backend::DetKv::from_store(Arc::new( crate::wallet_backend::kv_test_support::InMemoryKv::default(), ))); - let document_type = context - .dpns_contract + let dpns_contract = context.dpns_contract(); + let document_type = dpns_contract .document_type_for_name("domain") .expect("domain document type"); let page_from = |start_at_value| VotePollsByDocumentTypeQuery { - contract_id: context.dpns_contract.id(), + contract_id: context.dpns_contract().id(), document_type_name: document_type.name().to_owned(), index_name: document_type .find_contested_index() @@ -777,8 +777,8 @@ mod tests { name: &str, winner: Option<(ContestedDocumentVotePollWinnerInfo, BlockInfo)>, ) { - let document_type = context - .dpns_contract + let dpns_contract = context.dpns_contract(); + let document_type = dpns_contract .document_type_for_name("domain") .expect("domain document type"); let query = ContestedDocumentVotePollDriveQuery { @@ -793,7 +793,7 @@ mod tests { .clone(), index_values: vec![Value::from("dash"), Value::Text(name.to_owned())], document_type_name: document_type.name().to_owned(), - contract_id: context.dpns_contract.id(), + contract_id: context.dpns_contract().id(), }, allow_include_locked_and_abstaining_vote_tally: true, result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally, diff --git a/src/backend_task/contested_names/query_dpns_vote_contenders.rs b/src/backend_task/contested_names/query_dpns_vote_contenders.rs index 52760f99b..8eab0311a 100644 --- a/src/backend_task/contested_names/query_dpns_vote_contenders.rs +++ b/src/backend_task/contested_names/query_dpns_vote_contenders.rs @@ -20,7 +20,7 @@ impl AppContext { sdk: &Sdk, _sender: crate::utils::egui_mpsc::SenderAsync, ) -> Result<(), TaskError> { - let data_contract = self.dpns_contract.as_ref(); + let data_contract = self.dpns_contract(); let document_type = data_contract .document_type_for_name("domain") .map_err(|_| TaskError::DataContractNotFound)?; diff --git a/src/backend_task/contested_names/vote_on_dpns_name.rs b/src/backend_task/contested_names/vote_on_dpns_name.rs index a0e7e8fb4..a6ef82010 100644 --- a/src/backend_task/contested_names/vote_on_dpns_name.rs +++ b/src/backend_task/contested_names/vote_on_dpns_name.rs @@ -169,7 +169,7 @@ impl AppContext { name: &str, sdk: &Sdk, ) -> Result { - let vote_poll = dpns_vote_poll(&self.dpns_contract, name)?; + let vote_poll = dpns_vote_poll(&self.dpns_contract(), name)?; let normalized_label = convert_to_homograph_safe_chars(name); // Pre-flight: confirm Platform has an open poll for this label before diff --git a/src/backend_task/contract.rs b/src/backend_task/contract.rs index a21cd10bf..a18e9e1e5 100644 --- a/src/backend_task/contract.rs +++ b/src/backend_task/contract.rs @@ -14,6 +14,7 @@ use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; +use dash_sdk::dpp::data_contract::document_type::action_fees::ContractFeePot; use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; use dash_sdk::dpp::document::DocumentV0Getters; use dash_sdk::dpp::group::group_action::GroupAction; @@ -36,6 +37,15 @@ pub enum ContractTask { RegisterDataContract(DataContract, String, QualifiedIdentity, IdentityPublicKey), // contract, alias, identity, signing_key UpdateDataContract(DataContract, QualifiedIdentity, IdentityPublicKey), // contract, identity, signing_key SaveDataContract(DataContract, Option, InsertTokensToo), + /// Read both fee pots of a contract (protocol version 14). Read-only. + FetchContractFeePots(Identifier), + /// Pay out one fee pot of a contract, claimed by `identity`: the owner + /// pot by the contract owner, the moderators pot by a team member. + ClaimContractFees { + contract_id: Identifier, + pot: ContractFeePot, + identity: Box, + }, } impl AppContext { @@ -46,6 +56,17 @@ impl AppContext { sender: crate::utils::egui_mpsc::SenderAsync, ) -> Result { match task { + ContractTask::FetchContractFeePots(contract_id) => { + self.fetch_contract_fee_pots(sdk, contract_id).await + } + ContractTask::ClaimContractFees { + contract_id, + pot, + identity, + } => { + self.claim_contract_fees(sdk, contract_id, pot, *identity) + .await + } ContractTask::FetchContracts(identifiers) => { match DataContract::fetch_many(sdk, identifiers).await { Ok(data_contracts) => { @@ -76,8 +97,9 @@ impl AppContext { for data_contract in data_contracts { if let Some(contract) = &data_contract.1 { // Fetch the contract description from the Search Contract - let search_contract = &self.keyword_search_contract; + let search_contract = self.keyword_search_contract(); let document_query = DocumentQuery { + integer_range_clauses: vec![], sub_queries: Vec::new(), select: SelectProjection::documents(), data_contract: search_contract.clone(), diff --git a/src/backend_task/contract_fee_pots.rs b/src/backend_task/contract_fee_pots.rs new file mode 100644 index 000000000..312490c81 --- /dev/null +++ b/src/backend_task/contract_fee_pots.rs @@ -0,0 +1,223 @@ +//! Contract fee pots (protocol version 14): what a contract's document action +//! fees have paid into its owner pot and its moderators pot, and paying a pot +//! out to its recipients. + +use std::sync::Arc; + +use crate::backend_task::BackendTaskSuccessResult; +use crate::backend_task::error::TaskError; +use crate::context::AppContext; +use crate::context::feature_gate::FeatureGate; +use crate::model::contract_fee_pots::{SeatedModerators, has_elected_moderation}; +use crate::model::identity_key_usability::{KeyRequirements, SigningScope}; +use crate::model::qualified_identity::QualifiedIdentity; +use dash_sdk::Sdk; +use dash_sdk::dpp::data_contract::document_type::action_fees::ContractFeePot; +use dash_sdk::dpp::identity::accessors::{IdentityGettersV0, IdentitySettersV0}; +use dash_sdk::dpp::identity::{Purpose, SecurityLevel}; +use dash_sdk::platform::contract_fee_pots::ContractFeePots; +use dash_sdk::platform::transition::contract_fee_claim::ClaimContractFees; +use dash_sdk::platform::{Fetch, Identifier}; + +/// The key requirements of a fee claim: a CRITICAL authentication key, and +/// since a claim is not a document batch, not one bound to a contract. +fn fee_claim_key_requirements() -> KeyRequirements<'static> { + KeyRequirements::new( + Purpose::AUTHENTICATION, + &[SecurityLevel::CRITICAL], + SigningScope::NonBatch, + ) +} + +impl AppContext { + /// Both fee pots of `contract_id`, with the seated moderation team that + /// claims an elected contract's moderators pot. A contract nobody has paid + /// fees to reads as two empty pots. + pub(super) async fn fetch_contract_fee_pots( + &self, + sdk: &Sdk, + contract_id: Identifier, + ) -> Result { + if !FeatureGate::ContractFeePots.is_available(self) { + return Err(TaskError::ContractFeePotsNotSupported); + } + // A contract saved here without elected moderation has no seated + // team, so its team is not read. One not saved here is read anyway. + let elected = self + .get_unqualified_contract_by_id(&contract_id) + .ok() + .flatten() + .is_none_or(|contract| has_elected_moderation(&contract)); + // Only the pots are proved. The response metadata's epoch is not + // signed, so nothing is decided from it. + let (pots, seated_moderators) = + tokio::join!(ContractFeePots::fetch(sdk, contract_id), async { + if elected { + read_seated_moderators(sdk, contract_id).await + } else { + SeatedModerators::NotSeated + } + }); + Ok(BackendTaskSuccessResult::ContractFeePots { + contract_id, + pots: pots?.unwrap_or_default(), + seated_moderators, + }) + } + + /// Pay out `pot` of `contract_id`, claimed by `qualified_identity`, and + /// store the claimant's new balance. + pub(super) async fn claim_contract_fees( + &self, + sdk: &Sdk, + contract_id: Identifier, + pot: ContractFeePot, + qualified_identity: QualifiedIdentity, + ) -> Result { + if !FeatureGate::ContractFeePots.is_available(self) { + return Err(TaskError::ContractFeePotsNotSupported); + } + let identity_id = qualified_identity.identity.id(); + let signing_key = qualified_identity + .signing_key_now(fee_claim_key_requirements()) + .cloned() + .ok_or(TaskError::NoFeeClaimSigningKey)?; + let identity = qualified_identity.identity.clone(); + let signer = Arc::new(qualified_identity); + + let claimed = identity + .claim_contract_fees(sdk, contract_id, pot, Some(&signing_key), signer, None) + .await?; + + let claimant_balance = claimed.balances.get(&identity_id).copied(); + if let Some(balance) = claimant_balance { + self.persist_claimant_balance(&identity_id, balance)?; + } + + Ok(BackendTaskSuccessResult::ContractFeesClaimed { + contract_id, + pot, + claimant_balance, + remaining_credits: claimed.remaining_credits, + }) + } +} + +impl AppContext { + /// Store `balance`, the claimant's balance after a claim the network + /// paid out, on the record of `identity_id`. The claim is already paid + /// out, so a failure here is reported as + /// [`TaskError::ContractFeesClaimedButBalanceNotSaved`], not as a failed + /// claim. + fn persist_claimant_balance( + &self, + identity_id: &Identifier, + balance: dash_sdk::dpp::fee::Credits, + ) -> Result<(), TaskError> { + self.edit_local_qualified_identity(identity_id, |fresh| { + fresh.identity.set_balance(balance); + Ok(()) + }) + .map(|_| ()) + .map_err(|source| TaskError::ContractFeesClaimedButBalanceNotSaved { + source: Box::new(source), + }) + } +} + +/// The seated moderation team of `contract_id`, its leader and active +/// members: who Platform lets claim an elected contract's moderators pot. +/// +/// A failed read is not an error of the pots read: the pots still show, and +/// the claim is left to Platform to accept or refuse. +async fn read_seated_moderators(sdk: &Sdk, contract_id: Identifier) -> SeatedModerators { + match sdk.fetch_moderation_team(contract_id).await { + Ok(None) => SeatedModerators::NotSeated, + Ok(Some(team)) => { + let mut members = team.members; + members.insert(team.leader_id); + SeatedModerators::Seated(members) + } + Err(error) => { + tracing::debug!( + ?error, + %contract_id, + "Could not read the seated moderation team; leaving moderator claims to Platform" + ); + SeatedModerators::Unknown + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A claim the network paid out is reported as paid out, not as failed, + /// when storing the claimant's new balance on this device fails. + #[test] + fn a_claim_whose_balance_cannot_be_saved_is_reported_as_paid_out() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + + // Nothing is stored for this identity, so the write fails. + let result = ctx.persist_claimant_balance(&Identifier::random(), 1_000); + + assert!( + matches!( + result, + Err(TaskError::ContractFeesClaimedButBalanceNotSaved { .. }) + ), + "got {result:?}" + ); + } + + /// Before protocol version 14 is confirmed nothing is read or claimed: + /// an older network has no fee pots. + #[tokio::test] + async fn fee_pots_need_a_network_that_keeps_them() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + let sdk = Sdk::new_mock(); + + // Not known yet (0), and mainnet's 13. + for version in [0, 13] { + ctx.set_platform_protocol_version(version); + let fetched = ctx + .fetch_contract_fee_pots(&sdk, Identifier::random()) + .await; + assert!( + matches!(fetched, Err(TaskError::ContractFeePotsNotSupported)), + "protocol {version}: got {fetched:?}" + ); + + let identity = crate::backend_task::identity::key_limits_test_identity(); + let claimed = ctx + .claim_contract_fees(&sdk, Identifier::random(), ContractFeePot::Owner, identity) + .await; + assert!( + matches!(claimed, Err(TaskError::ContractFeePotsNotSupported)), + "protocol {version}: got {claimed:?}" + ); + } + } + + /// A claimant with no CRITICAL unbound key is refused before anything is + /// signed or broadcast. + #[tokio::test] + async fn a_claim_needs_a_critical_unbound_key() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + ctx.set_platform_protocol_version(dash_sdk::dpp::version::v14::PROTOCOL_VERSION_14); + let sdk = Sdk::new_mock(); + let identity = crate::backend_task::identity::key_limits_test_identity(); + + let claimed = ctx + .claim_contract_fees(&sdk, Identifier::random(), ContractFeePot::Owner, identity) + .await; + assert!( + matches!(claimed, Err(TaskError::NoFeeClaimSigningKey)), + "got {claimed:?}" + ); + } +} diff --git a/src/backend_task/dashpay.rs b/src/backend_task/dashpay.rs index ae474f2fe..afe1d2ee9 100644 --- a/src/backend_task/dashpay.rs +++ b/src/backend_task/dashpay.rs @@ -32,7 +32,7 @@ use errors::DashPayError; /// Every `contactRequest` read in this module goes through here, so the /// contract handle and the error attribution are stated once. fn contact_request_query(app_context: &AppContext) -> Result { - DocumentQuery::new(app_context.dashpay_contract.clone(), "contactRequest").map_err(|e| { + DocumentQuery::new(app_context.dashpay_contract(), "contactRequest").map_err(|e| { DashPayError::QueryCreation { query_target: "DashPay contactRequest", source: Box::new(e.into()), @@ -40,6 +40,9 @@ fn contact_request_query(app_context: &AppContext) -> Result Ok(true), Err(_) => Ok(false), } diff --git a/src/backend_task/dashpay/contact_info.rs b/src/backend_task/dashpay/contact_info.rs index 3e191237f..c22474831 100644 --- a/src/backend_task/dashpay/contact_info.rs +++ b/src/backend_task/dashpay/contact_info.rs @@ -17,14 +17,13 @@ use dash_sdk::dpp::document::{ }; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; -use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; use dash_sdk::dpp::platform_value::{Bytes32, Value}; use dash_sdk::drive::query::{OrderClause, WhereClause, WhereOperator}; use dash_sdk::platform::documents::transitions::DocumentCreateTransitionBuilder; use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; use dash_sdk::platform::{Document, DocumentQuery, FetchMany, Identifier}; use dash_sdk::query_types::Documents; -use std::collections::{BTreeMap, HashSet}; +use std::collections::BTreeMap; use std::future::Future; use std::sync::Arc; use zeroize::Zeroizing; @@ -490,7 +489,7 @@ async fn lookup_contact_info( identity: &QualifiedIdentity, contact_user_id: Identifier, ) -> Result { - let dashpay_contract = app_context.dashpay_contract.clone(); + let dashpay_contract = app_context.dashpay_contract(); let identity_id = identity.identity.id(); let mut query = DocumentQuery::new(dashpay_contract, "contactInfo").map_err(|e| { DashPayError::QueryCreation { @@ -591,7 +590,7 @@ pub async fn create_or_update_contact_info( contact_user_id: Identifier, update: ContactInfoUpdate, ) -> Result { - let dashpay_contract = app_context.dashpay_contract.clone(); + let dashpay_contract = app_context.dashpay_contract(); let identity_id = identity.identity.id(); let lookup = lookup_contact_info(app_context, sdk, &identity, contact_user_id).await?; let (found_existing_doc, derivation_index, enc_user_id_key, private_data_key) = @@ -634,20 +633,7 @@ pub async fn create_or_update_contact_info( )); } - // Get signing key — accept any key type (BLS, ECDSA, EDDSA) since - // Platform accepts all for document state transitions. - let signing_key = identity - .identity - .get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([ - SecurityLevel::CRITICAL, - SecurityLevel::HIGH, - SecurityLevel::MEDIUM, - ]), - KeyType::all_key_types().into(), - false, - ) + let signing_key = contact_info_signing_key(&identity, &dashpay_contract) .ok_or_else(|| TaskError::DashPay(DashPayError::MissingAuthenticationKey))?; // Create document properties @@ -714,6 +700,11 @@ pub async fn create_or_update_contact_info( let mut rng = StdRng::from_entropy(); let entropy = Bytes32::random_with_rng(&mut rng); + // Before protocol version 14 this entropy-only id is the document's id, + // which Platform recomputes from the entropy. From protocol version 14 + // (`Document::document_id_depends_on_nonce`) the id also commits to the + // identity contract nonce, so this one is only a placeholder that building + // the create transition replaces. Read the id from the confirmed document. let document_id = Document::generate_document_id_v0( &dashpay_contract.id(), &identity_id, @@ -722,6 +713,8 @@ pub async fn create_or_update_contact_info( ); let document = DppDocument::V0(DocumentV0 { + moderated_at: None, + moderated_by: None, id: document_id, owner_id: identity_id, creator_id: None, @@ -775,6 +768,15 @@ pub async fn create_or_update_contact_info( }) } +/// The key to sign a contactInfo document with, of any key type: Platform +/// accepts all of them for document transitions. +fn contact_info_signing_key<'i>( + identity: &'i QualifiedIdentity, + dashpay_contract: &dash_sdk::platform::DataContract, +) -> Option<&'i dash_sdk::platform::IdentityPublicKey> { + identity.document_action_signing_key(dashpay_contract, "contactInfo", None) +} + #[cfg(test)] mod tests { use super::*; @@ -843,6 +845,8 @@ mod tests { properties.insert("privateData".to_string(), private_data); } DppDocument::V0(DocumentV0 { + moderated_at: None, + moderated_by: None, id: id(1), owner_id: id(2), creator_id: None, @@ -878,6 +882,8 @@ mod tests { ), ]); DppDocument::V0(DocumentV0 { + moderated_at: None, + moderated_by: None, id: id(document_id), owner_id: id(254), creator_id: None, @@ -1272,4 +1278,19 @@ mod tests { }) )); } + + /// Regression: the contactInfo key choice admitted MEDIUM keys, which the + /// document type refuses, and preferred unlimited keys, so an unlimited + /// MEDIUM key beat a held limited HIGH key and the save was refused. + #[test] + fn a_contact_info_is_signed_with_a_key_its_document_type_accepts() { + use crate::context::test_support::{auth_key, dashpay_contract, identity_holding}; + use dash_sdk::dpp::identity::SecurityLevel; + + let limited_high = auth_key(1, SecurityLevel::HIGH).with_limits(Some(5_000), None); + let identity = identity_holding(&[limited_high, auth_key(2, SecurityLevel::MEDIUM)]); + + let chosen = contact_info_signing_key(&identity, &dashpay_contract()); + assert_eq!(chosen.map(|key| key.id()), Some(1)); + } } diff --git a/src/backend_task/dashpay/contact_requests.rs b/src/backend_task/dashpay/contact_requests.rs index 50873710e..4a4b02db0 100644 --- a/src/backend_task/dashpay/contact_requests.rs +++ b/src/backend_task/dashpay/contact_requests.rs @@ -289,7 +289,7 @@ pub async fn send_contact_request_with_proof( } // Step 3: Check if a contact request already exists - let dashpay_contract = app_context.dashpay_contract.clone(); + let dashpay_contract = app_context.dashpay_contract(); let mut existing_query = contact_request_query(app_context)?; existing_query = existing_query @@ -520,7 +520,11 @@ pub async fn send_contact_request_with_proof( let mut rng = StdRng::from_entropy(); let entropy = Bytes32::random_with_rng(&mut rng); - // Generate deterministic document ID based on entropy + // Before protocol version 14 this entropy-only id is the document's id, + // which Platform recomputes from the entropy. From protocol version 14 + // (`Document::document_id_depends_on_nonce`) the id also commits to the + // identity contract nonce, so this one is only a placeholder that building + // the create transition replaces. Read the id from the confirmed document. let document_id = Document::generate_document_id_v0( &dashpay_contract.id(), &identity.identity.id(), @@ -530,6 +534,8 @@ pub async fn send_contact_request_with_proof( // Create the document let document = DppDocument::V0(DocumentV0 { + moderated_at: None, + moderated_by: None, id: document_id, owner_id: identity.identity.id(), creator_id: None, @@ -709,16 +715,8 @@ pub async fn accept_contact_request( }); } - // Get an AUTHENTICATION key for signing the state transition - // Platform requires CRITICAL or HIGH security level for document creation let signing_key = identity - .identity - .get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL, SecurityLevel::HIGH]), - KeyType::all_key_types().into(), - false, - ) + .document_action_signing_key(&app_context.dashpay_contract(), "contactRequest", None) .ok_or_else(|| TaskError::DashPay(DashPayError::MissingAuthenticationKey))? .clone(); @@ -1289,6 +1287,8 @@ mod tests { properties.insert("toUserId".to_string(), Value::Identifier(to.to_buffer())); } DppDocument::V0(DocumentV0 { + moderated_at: None, + moderated_by: None, id: id(99), owner_id: owner, creator_id: None, @@ -1319,7 +1319,7 @@ mod tests { operator: WhereOperator::Equal, value: Value::Text(value.to_string()), }; - let mut query = DocumentQuery::new(ctx.dpns_contract.clone(), "domain") + let mut query = DocumentQuery::new(ctx.dpns_contract(), "domain") .expect("domain query") .with_where(equals("normalizedParentDomainName", "dash")) .with_where(equals("normalizedLabel", normalized)); diff --git a/src/backend_task/dashpay/contacts.rs b/src/backend_task/dashpay/contacts.rs index 6de7aa6d1..642f1a18b 100644 --- a/src/backend_task/dashpay/contacts.rs +++ b/src/backend_task/dashpay/contacts.rs @@ -145,7 +145,7 @@ pub async fn load_contacts( identity: QualifiedIdentity, ) -> Result { let identity_id = identity.identity.id(); - let dashpay_contract = app_context.dashpay_contract.clone(); + let dashpay_contract = app_context.dashpay_contract(); // Query for contact requests where we are the sender (ownerId) let mut outgoing_query = contact_request_query(app_context)?; diff --git a/src/backend_task/dashpay/encryption.rs b/src/backend_task/dashpay/encryption.rs index ca0f060fd..e6f11be17 100644 --- a/src/backend_task/dashpay/encryption.rs +++ b/src/backend_task/dashpay/encryption.rs @@ -1,10 +1,10 @@ use aes_gcm::aes::Aes256; use bip39::rand::{self, RngCore}; use cbc; -use dash_sdk::dpp::dashcore::secp256k1::{PublicKey, Secp256k1, SecretKey}; use dash_sdk::dpp::identity::IdentityPublicKey; use dash_sdk::dpp::identity::KeyType; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dash_sdk::dpp::secp256k1::{PublicKey, SecretKey}; use sha2::{Digest, Sha256}; use zeroize::Zeroizing; @@ -18,11 +18,13 @@ pub fn generate_ecdh_shared_key( private_key: &[u8], public_key: &IdentityPublicKey, ) -> Result, String> { - let _secp = Secp256k1::new(); - // Parse the private key - let secret_key = - SecretKey::from_slice(private_key).map_err(|e| format!("Invalid private key: {}", e))?; + let secret_key = SecretKey::from_secret_bytes( + private_key + .try_into() + .map_err(|_| "Invalid private key length".to_string())?, + ) + .map_err(|e| format!("Invalid private key: {}", e))?; // Get the public key data - only works for full secp256k1 keys match public_key.key_type() { @@ -34,7 +36,7 @@ pub fn generate_ecdh_shared_key( // Perform ECDH to get shared secret. The 64-byte point is secret; // hold it in `Zeroizing` so it is wiped once the key is hashed out. let shared_secret = Zeroizing::new( - dash_sdk::dpp::dashcore::secp256k1::ecdh::shared_secret_point( + dash_sdk::dpp::secp256k1::ecdh::shared_secret_point( &public_key, &secret_key, ), @@ -295,7 +297,7 @@ pub fn decrypt_account_label( mod tests { use super::*; use bip39::rand::{self, RngCore}; - use dash_sdk::dpp::dashcore::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use dash_sdk::dpp::secp256k1::{PublicKey, SecretKey}; fn generate_test_shared_key() -> [u8; 32] { let mut shared_key = [0u8; 32]; @@ -304,14 +306,13 @@ mod tests { } fn generate_test_key_pair() -> (SecretKey, PublicKey) { - let secp = Secp256k1::new(); - let secret_key = SecretKey::from_slice(&[ + let secret_key = SecretKey::from_secret_bytes([ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, ]) .unwrap(); - let public_key = PublicKey::from_secret_key(&secp, &secret_key); + let public_key = PublicKey::from_secret_key(&secret_key); (secret_key, public_key) } @@ -380,8 +381,6 @@ mod tests { /// wipes the sha2 digest) cannot silently alter the derived material. #[test] fn ecdh_shared_key_matches_pinned_vector() { - let secp = Secp256k1::new(); - // Our private key: 0x01..=0x20 (same fixed vector the suite uses). let private_key = [ 0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, @@ -391,13 +390,13 @@ mod tests { // Counterpart key derived from a different fixed secret so the ECDH // point is non-degenerate. - let counterpart_secret = SecretKey::from_slice(&[ + let counterpart_secret = SecretKey::from_secret_bytes([ 0xA1u8, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, ]) .unwrap(); - let counterpart_pub = PublicKey::from_secret_key(&secp, &counterpart_secret); + let counterpart_pub = PublicKey::from_secret_key(&counterpart_secret); let counterpart_ipk = ecdsa_identity_public_key(&counterpart_pub); let shared_key = generate_ecdh_shared_key(&private_key, &counterpart_ipk) diff --git a/src/backend_task/dashpay/errors.rs b/src/backend_task/dashpay/errors.rs index caa14ffaa..a8561ed0c 100644 --- a/src/backend_task/dashpay/errors.rs +++ b/src/backend_task/dashpay/errors.rs @@ -1,4 +1,4 @@ -use dash_sdk::dpp::dashcore::secp256k1; +use dash_sdk::dpp::secp256k1; use dash_sdk::platform::Identifier; use thiserror::Error; diff --git a/src/backend_task/dashpay/payments.rs b/src/backend_task/dashpay/payments.rs index 31db1e4de..37d27de5e 100644 --- a/src/backend_task/dashpay/payments.rs +++ b/src/backend_task/dashpay/payments.rs @@ -92,7 +92,7 @@ pub async fn derive_contact_payment_address( contact_id: Identifier, ) -> Result<(Address, u32), String> { // Fetch the contact request from the contact to us (they sent us their encrypted xpub) - let dashpay_contract = app_context.dashpay_contract.clone(); + let dashpay_contract = app_context.dashpay_contract(); let mut query = DocumentQuery::new(dashpay_contract.clone(), "contactRequest") .map_err(|e| format!("Failed to create query: {}", e))?; @@ -185,8 +185,8 @@ pub async fn derive_contact_payment_address( // Create extended public key from components // This is simplified - in production you'd properly reconstruct with all fields - use dash_sdk::dpp::dashcore::secp256k1::{PublicKey, Secp256k1}; use dash_sdk::dpp::key_wallet::bip32::{ChainCode, ChildNumber, ExtendedPubKey, Fingerprint}; + use dash_sdk::dpp::secp256k1::{PublicKey, Secp256k1}; let _secp = Secp256k1::new(); let pubkey = @@ -199,7 +199,7 @@ pub async fn derive_contact_payment_address( parent_fingerprint: Fingerprint::default(), child_number: ChildNumber::from_normal_idx(0) .expect("invariant: BIP32 child index 0 is below 2^31"), - public_key: pubkey, + public_key: pubkey.into(), chain_code: ChainCode::from(chain_code), }; diff --git a/src/backend_task/dashpay/profile.rs b/src/backend_task/dashpay/profile.rs index f67569e0b..eb6605f89 100644 --- a/src/backend_task/dashpay/profile.rs +++ b/src/backend_task/dashpay/profile.rs @@ -21,7 +21,7 @@ pub async fn load_profile( let identity_id = identity.identity.id(); let _operation = app_context.lock_identity_profile(identity_id).await; let name_revision = app_context.begin_identity_profile_load(identity_id); - let dashpay_contract = app_context.dashpay_contract.clone(); + let dashpay_contract = app_context.dashpay_contract(); // Query for profile document owned by this identity let mut profile_query = DocumentQuery::new(dashpay_contract, "profile").map_err(|e| { @@ -193,13 +193,12 @@ pub async fn update_profile( "Failed to cache the avatar fetched for a profile update" ); } - let mut query = - DocumentQuery::new(app_context.dashpay_contract.clone(), "profile").map_err(|e| { - DashPayError::QueryCreation { - query_target: "DashPay profile", - source: Box::new(e.into()), - } - })?; + let mut query = DocumentQuery::new(app_context.dashpay_contract(), "profile").map_err(|e| { + DashPayError::QueryCreation { + query_target: "DashPay profile", + source: Box::new(e.into()), + } + })?; query = query.with_where(WhereClause { field: "$ownerId".to_string(), operator: WhereOperator::Equal, @@ -321,7 +320,7 @@ pub async fn fetch_contact_profile( _identity: QualifiedIdentity, // May be needed for future privacy features contact_id: Identifier, ) -> Result { - let dashpay_contract = app_context.dashpay_contract.clone(); + let dashpay_contract = app_context.dashpay_contract(); // Query for the contact's profile document let mut query = DocumentQuery::new(dashpay_contract, "profile").map_err(|e| { @@ -358,7 +357,7 @@ pub async fn search_profiles( sdk: &Sdk, search_query: String, ) -> Result { - let dashpay_contract = app_context.dashpay_contract.clone(); + let dashpay_contract = app_context.dashpay_contract(); let mut results: Vec<(Identifier, Option, String)> = Vec::new(); let query_trimmed = search_query.trim(); @@ -432,7 +431,7 @@ mod tests { "Alice", Value::Identifier(holder.to_buffer()), ); - let mut prefix_query = DocumentQuery::new(ctx.dpns_contract.clone(), "domain") + let mut prefix_query = DocumentQuery::new(ctx.dpns_contract(), "domain") .expect("domain query") .with_where(WhereClause { field: "normalizedParentDomainName".to_string(), @@ -563,6 +562,8 @@ mod tests { fn profile_removal_is_rejected_instead_of_silently_retaining_fields() { for field in ["displayName", "publicMessage", "avatarUrl"] { let existing = Document::V0(dash_sdk::dpp::document::DocumentV0 { + moderated_at: None, + moderated_by: None, properties: [(field.to_string(), Value::Text("existing value".into()))].into(), ..Default::default() }); @@ -582,6 +583,8 @@ mod tests { #[test] fn profile_replacement_is_allowed_with_unchanged_optional_fields() { let existing = Document::V0(dash_sdk::dpp::document::DocumentV0 { + moderated_at: None, + moderated_by: None, properties: [("displayName".to_string(), Value::Text("Alice".into()))].into(), ..Default::default() }); diff --git a/src/backend_task/document.rs b/src/backend_task/document.rs index 59c8bfef9..c07e464ca 100644 --- a/src/backend_task/document.rs +++ b/src/backend_task/document.rs @@ -4,15 +4,21 @@ use crate::backend_task::{ await_network_request_with_timeout, }; use crate::context::AppContext; +use crate::model::fee_estimation::checked_action_fee_agreement; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::request_type::RequestType; +use dash_sdk::Error as SdkError; use dash_sdk::Sdk; -use dash_sdk::dpp::data_contract::document_type::DocumentType; use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dash_sdk::dpp::data_contract::document_type::action_fees::agreement::DocumentActionFeeAgreement; use dash_sdk::dpp::document::{DocumentV0Getters, DocumentV0Setters}; use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::tokens::token_payment_info::TokenPaymentInfo; +use dash_sdk::dpp::{ + data_contract::document_type::DocumentType, + state_transition::batch_transition::batched_transition::document_transition_action_type::DocumentTransitionActionType, +}; use dash_sdk::drive::query::SelectProjection; use dash_sdk::platform::documents::transitions::DocumentCreateResult; use dash_sdk::platform::documents::transitions::DocumentCreateTransitionBuilder; @@ -43,6 +49,10 @@ pub enum DocumentTask { data_contract: Arc, qualified_identity: QualifiedIdentity, identity_key: IdentityPublicKey, + /// The document action fee the user was shown and agreed to (protocol + /// version 14). Required when the document type charges a fee for + /// the action: the backend never agrees on the user's behalf. + action_fee_agreement: Option, }, DeleteDocument { document_id: Identifier, @@ -51,6 +61,10 @@ pub enum DocumentTask { qualified_identity: QualifiedIdentity, identity_key: IdentityPublicKey, token_payment_info: Option, + /// The document action fee the user was shown and agreed to (protocol + /// version 14). Required when the document type charges a fee for + /// the action: the backend never agrees on the user's behalf. + action_fee_agreement: Option, }, ReplaceDocument { document: Document, @@ -59,6 +73,10 @@ pub enum DocumentTask { qualified_identity: QualifiedIdentity, identity_key: IdentityPublicKey, token_payment_info: Option, + /// The document action fee the user was shown and agreed to (protocol + /// version 14). Required when the document type charges a fee for + /// the action: the backend never agrees on the user's behalf. + action_fee_agreement: Option, }, TransferDocument { document_id: Identifier, @@ -68,6 +86,10 @@ pub enum DocumentTask { qualified_identity: QualifiedIdentity, identity_key: IdentityPublicKey, token_payment_info: Option, + /// The document action fee the user was shown and agreed to (protocol + /// version 14). Required when the document type charges a fee for + /// the action: the backend never agrees on the user's behalf. + action_fee_agreement: Option, }, PurchaseDocument { price: Credits, @@ -77,6 +99,10 @@ pub enum DocumentTask { qualified_identity: QualifiedIdentity, identity_key: IdentityPublicKey, token_payment_info: Option, + /// The document action fee the user was shown and agreed to (protocol + /// version 14). Required when the document type charges a fee for + /// the action: the backend never agrees on the user's behalf. + action_fee_agreement: Option, }, SetDocumentPrice { price: Credits, @@ -86,6 +112,10 @@ pub enum DocumentTask { qualified_identity: QualifiedIdentity, identity_key: IdentityPublicKey, token_payment_info: Option, + /// The document action fee the user was shown and agreed to (protocol + /// version 14). Required when the document type charges a fee for + /// the action: the backend never agrees on the user's behalf. + action_fee_agreement: Option, }, FetchDocuments(DocumentQuery), FetchDocumentsPage(DocumentQuery), @@ -102,6 +132,7 @@ impl AppContext { document_id: Identifier, ) -> Result { let document_query = DocumentQuery { + integer_range_clauses: vec![], sub_queries: Vec::new(), select: SelectProjection::documents(), data_contract, @@ -132,6 +163,22 @@ impl AppContext { &self, task: DocumentTask, sdk: &Sdk, + ) -> Result { + match self.run_document_task_inner(task, sdk).await { + // The multiplier the refusal names is unproven node data: it only + // triggers a proved epoch refresh, never feeds the cache itself. + Err(refusal @ TaskError::DocumentActionFeeMultiplierRose { .. }) => { + let refreshed = self.refresh_current_epoch(sdk).await.map(|_| ()); + Err(fee_refusal_after_refresh(refusal, refreshed)) + } + result => result, + } + } + + async fn run_document_task_inner( + &self, + task: DocumentTask, + sdk: &Sdk, ) -> Result { match task { DocumentTask::FetchDocuments(document_query) => await_network_request_with_timeout( @@ -191,7 +238,14 @@ impl AppContext { data_contract, qualified_identity, identity_key, + action_fee_agreement, } => { + let contest_fund = crate::model::fee_estimation::document_create_contest_fund_cap( + document_type.as_ref(), + &document, + sdk.version(), + ) + .map_err(|error| dash_sdk::Error::Protocol(*error))?; let mut builder = DocumentCreateTransitionBuilder::new( data_contract, document_type.name().to_string(), @@ -203,10 +257,24 @@ impl AppContext { builder = builder.with_token_payment_info(token_payment); } - let maybe_options = self.state_transition_options(); + let mut maybe_options = self.state_transition_options(); + if let Some(cap) = contest_fund { + maybe_options + .get_or_insert_with(Default::default) + .contest_fund = Some(cap); + } if let Some(options) = maybe_options { builder = builder.with_state_transition_creation_options(options); } + // After the options: setting them replaces the struct the + // agreement is kept in. + if let Some(agreement) = checked_action_fee_agreement( + document_type.as_ref(), + DocumentTransitionActionType::Create, + action_fee_agreement, + )? { + builder = builder.with_action_fee_agreement(agreement); + } let result = sdk .document_create(builder, &identity_key, &qualified_identity) @@ -229,6 +297,7 @@ impl AppContext { qualified_identity, identity_key, token_payment_info, + action_fee_agreement, } => { let mut builder = DocumentDeleteTransitionBuilder::new( data_contract, @@ -245,6 +314,15 @@ impl AppContext { if let Some(options) = maybe_options { builder = builder.with_state_transition_creation_options(options); } + // After the options: setting them replaces the struct the + // agreement is kept in. + if let Some(agreement) = checked_action_fee_agreement( + document_type.as_ref(), + DocumentTransitionActionType::Delete, + action_fee_agreement, + )? { + builder = builder.with_action_fee_agreement(agreement); + } let result = sdk .document_delete(builder, &identity_key, &qualified_identity) @@ -269,6 +347,7 @@ impl AppContext { qualified_identity, identity_key, token_payment_info, + action_fee_agreement, } => { let mut builder = DocumentReplaceTransitionBuilder::new( data_contract, @@ -284,6 +363,15 @@ impl AppContext { if let Some(options) = maybe_options { builder = builder.with_state_transition_creation_options(options); } + // After the options: setting them replaces the struct the + // agreement is kept in. + if let Some(agreement) = checked_action_fee_agreement( + document_type.as_ref(), + DocumentTransitionActionType::Replace, + action_fee_agreement, + )? { + builder = builder.with_action_fee_agreement(agreement); + } let result = sdk .document_replace(builder, &identity_key, &qualified_identity) @@ -309,6 +397,7 @@ impl AppContext { qualified_identity, identity_key, token_payment_info, + action_fee_agreement, } => { let document = self .fetch_document_for_mutation( @@ -334,6 +423,15 @@ impl AppContext { if let Some(options) = maybe_options { builder = builder.with_state_transition_creation_options(options); } + // After the options: setting them replaces the struct the + // agreement is kept in. + if let Some(agreement) = checked_action_fee_agreement( + document_type.as_ref(), + DocumentTransitionActionType::Transfer, + action_fee_agreement, + )? { + builder = builder.with_action_fee_agreement(agreement); + } let result = sdk .document_transfer(builder, &identity_key, &qualified_identity) @@ -359,6 +457,7 @@ impl AppContext { qualified_identity, identity_key, token_payment_info, + action_fee_agreement, } => { let document = self .fetch_document_for_mutation( @@ -385,6 +484,15 @@ impl AppContext { if let Some(options) = maybe_options { builder = builder.with_state_transition_creation_options(options); } + // After the options: setting them replaces the struct the + // agreement is kept in. + if let Some(agreement) = checked_action_fee_agreement( + document_type.as_ref(), + DocumentTransitionActionType::Purchase, + action_fee_agreement, + )? { + builder = builder.with_action_fee_agreement(agreement); + } let result = sdk .document_purchase(builder, &identity_key, &qualified_identity) @@ -410,6 +518,7 @@ impl AppContext { qualified_identity, identity_key, token_payment_info, + action_fee_agreement, } => { let document = self .fetch_document_for_mutation( @@ -435,6 +544,15 @@ impl AppContext { if let Some(options) = maybe_options { builder = builder.with_state_transition_creation_options(options); } + // After the options: setting them replaces the struct the + // agreement is kept in. + if let Some(agreement) = checked_action_fee_agreement( + document_type.as_ref(), + DocumentTransitionActionType::UpdatePrice, + action_fee_agreement, + )? { + builder = builder.with_action_fee_agreement(agreement); + } let result = sdk .document_set_price(builder, &identity_key, &qualified_identity) @@ -456,8 +574,78 @@ impl AppContext { } } +/// The error to report for a fee multiplier refusal once the proved epoch +/// refresh it triggered has finished: the refusal itself when the cache now +/// holds the network's multiplier, otherwise a variant saying the contract +/// fee could not be updated (the cache keeps its previous value). +fn fee_refusal_after_refresh( + refusal: TaskError, + refreshed: Result<(), Box>, +) -> TaskError { + match (refusal, refreshed) { + (refusal, Ok(())) => refusal, + ( + TaskError::DocumentActionFeeMultiplierRose { + increase_tolerance_percent, + source_error, + .. + }, + Err(refresh_error), + ) => { + tracing::warn!( + refusal = ?source_error, + "Network refused a document action for a risen fee multiplier" + ); + TaskError::DocumentActionFeeMultiplierNotRefreshed { + increase_tolerance_percent, + source_error: refresh_error, + } + } + (other, Err(_)) => other, + } +} + #[cfg(test)] mod tests { + fn fee_refusal() -> TaskError { + TaskError::DocumentActionFeeMultiplierRose { + known_fee_multiplier_permille: 1000, + current_fee_multiplier_permille: 1500, + increase_tolerance_percent: 20, + source_error: Box::new(dash_sdk::Error::Generic("rose".to_string())), + } + } + + /// After a proved refresh the refusal is reported as is: the cache holds + /// the network's multiplier, so trying again quotes the current fee. + #[test] + fn a_fee_refusal_stands_once_the_epoch_is_refreshed() { + let error = fee_refusal_after_refresh(fee_refusal(), Ok(())); + assert!(matches!( + error, + TaskError::DocumentActionFeeMultiplierRose { .. } + )); + assert!(error.to_string().contains("has been updated")); + } + + /// When the proved refresh fails, the user is told the contract fee + /// could not be updated rather than that it was. + #[test] + fn a_failed_epoch_refresh_says_the_fee_was_not_updated() { + let error = fee_refusal_after_refresh( + fee_refusal(), + Err(Box::new(dash_sdk::Error::Generic("offline".to_string()))), + ); + assert!(matches!( + error, + TaskError::DocumentActionFeeMultiplierNotRefreshed { + increase_tolerance_percent: 20, + .. + } + )); + assert!(error.to_string().contains("could not be updated")); + } + use super::*; #[tokio::test] diff --git a/src/backend_task/error.rs b/src/backend_task/error.rs index fb6cb61a6..444e1d9c6 100644 --- a/src/backend_task/error.rs +++ b/src/backend_task/error.rs @@ -14,9 +14,12 @@ use dash_sdk::dashcore_rpc; use dash_sdk::dpp::ProtocolError; use dash_sdk::dpp::consensus::ConsensusError; use dash_sdk::dpp::consensus::basic::basic_error::BasicError; +use dash_sdk::dpp::consensus::signature::SignatureError; use dash_sdk::dpp::consensus::state::state_error::StateError; use dash_sdk::dpp::dashcore; use dash_sdk::dpp::dashcore::Network; +use dash_sdk::dpp::fee::Credits; +use dash_sdk::dpp::identity::KeyID; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::Identifier; use platform_wallet::wallet::asset_lock::tracked::TrackedAssetLock; @@ -43,6 +46,108 @@ impl DapiAddressAvailability { } } +/// The token operation a wallet-runtime token failure belongs to. +/// +/// Upstream `PlatformWalletError::TokenOperationFailed` names the operation with +/// a fixed `&'static str` label (a discriminator, not a message); this is its +/// typed form, so each failure gets a complete, translatable sentence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TokenOperationKind { + Claim, + Mint, + Burn, + Transfer, + Freeze, + Unfreeze, + DestroyFrozenFunds, + Pause, + Resume, + SetPrice, + Purchase, + ConfigUpdate, + /// A label this build does not know (a newer upstream operation). The + /// raw label is kept for `Debug` and logs, never shown to the user. + Unrecognized { + label: &'static str, + }, +} + +impl TokenOperationKind { + /// The kind for the operation label upstream puts in + /// `PlatformWalletError::TokenOperationFailed::operation`. + /// + /// Upstream exposes the operation only as this `&'static str` (no typed + /// enum), so every label it uses is matched here; an unknown one is + /// logged at warn and kept as [`Self::Unrecognized`], never silently + /// folded into another operation. + pub fn from_upstream_label(label: &'static str) -> Self { + match label { + "claim" => Self::Claim, + "mint" => Self::Mint, + "burn" => Self::Burn, + "transfer" => Self::Transfer, + "freeze" => Self::Freeze, + "unfreeze" => Self::Unfreeze, + "destroy frozen funds" => Self::DestroyFrozenFunds, + "pause" => Self::Pause, + "resume" => Self::Resume, + "set price" => Self::SetPrice, + "purchase" => Self::Purchase, + "config update" => Self::ConfigUpdate, + label => { + tracing::warn!( + label, + "Unrecognized upstream token operation label; showing a generic message" + ); + Self::Unrecognized { label } + } + } + } + + /// The user-facing sentence for a failure of this operation. + fn failure_message(self) -> &'static str { + match self { + Self::Claim => "The token claim did not complete. Check your connection and try again.", + Self::Mint => { + "Minting the tokens did not complete. Check your connection and try again." + } + Self::Burn => { + "Burning the tokens did not complete. Check your connection and try again." + } + Self::Transfer => { + "The token transfer did not complete. Check your connection and try again." + } + Self::Freeze => { + "Freezing the token account did not complete. Check your connection and try again." + } + Self::Unfreeze => { + "Unfreezing the token account did not complete. Check your connection and try again." + } + Self::DestroyFrozenFunds => { + "Destroying the frozen tokens did not complete. Check your connection and try again." + } + Self::Pause => { + "Pausing the token did not complete. Check your connection and try again." + } + Self::Resume => { + "Resuming the token did not complete. Check your connection and try again." + } + Self::SetPrice => { + "Setting the token price did not complete. Check your connection and try again." + } + Self::Purchase => { + "The token purchase did not complete. Check your connection and try again." + } + Self::ConfigUpdate => { + "Updating the token settings did not complete. Check your connection and try again." + } + Self::Unrecognized { .. } => { + "The token operation did not complete. Check your connection and try again." + } + } + } +} + /// Why an existing DashPay `contactInfo` payload could not be preserved. #[derive(Debug, Error)] pub enum ContactInfoReadError { @@ -2397,248 +2502,783 @@ pub enum TaskError { }, // ────────────────────────────────────────────────────────────────────────── - // Token query errors + // Signing key limits and contract bounds (protocol version 14) // ────────────────────────────────────────────────────────────────────────── - /// Querying token data from the platform failed. - #[error("Could not retrieve token information from the platform. Please retry.")] - TokenQueryError { detail: String }, - - /// The token does not have a perpetual distribution configured — no rewards to claim. - #[error("This token does not have perpetual distribution, so there are no rewards to claim.")] - TokenNoPerpetualDistribution, - - /// The recipient identity does not exist on Platform (e.g. during a token mint). + /// The signing key's expiry time has passed. #[error( - "The recipient identity `{recipient_id}` does not exist on the platform. \ - Check the ID and try again, or create the identity first." + "The key used to sign this action expired and can no longer sign. Choose a different key, or add a new key to this identity, then try again." )] - TokenRecipientIdentityNotFound { - recipient_id: String, + SigningKeyExpired { + key_id: KeyID, + /// When the key expired, in milliseconds since the Unix epoch. + expired_at_ms: u64, #[source] source_error: Box, }, - /// The identity's token account is not frozen, so an unfreeze / destroy-frozen action - /// cannot proceed. + /// The signing key has spent its whole budget. #[error( - "Identity `{identity_id}` is not frozen for token `{token_id}`, so `{action}` cannot proceed. \ - Refresh the frozen-account list and try again." + "The key used to sign this action has used up its spending limit and can no longer sign. Raise the key's spending limit or choose a different key, then try again." )] - TokenAccountNotFrozen { - identity_id: String, - token_id: String, - action: String, + SigningKeyBudgetExhausted { + key_id: KeyID, #[source] source_error: Box, }, - // ────────────────────────────────────────────────────────────────────────── - // Contract schema errors - // ────────────────────────────────────────────────────────────────────────── - /// The contract structure does not match expectations (e.g. missing contested index). - #[error("The contract structure is unexpected. Please update the application.")] - ContractSchemaMismatch { detail: &'static str }, - - /// Platform returned a different contract after accepting an update. + /// The action costs more than the signing key's remaining budget. #[error( - "The platform returned contract {returned} instead of {expected}. Please try the update again." + "This action costs {required_dash}, but the key used to sign it has only {remaining_dash} of its spending limit left. Raise the key's spending limit or choose a different key, then try again.", + required_dash = format_credits_as_dash(*.required_budget), + remaining_dash = format_credits_as_dash(*.remaining_budget) )] - UpdatedContractIdMismatch { - expected: Identifier, - returned: Identifier, + SigningKeyBudgetExceeded { + key_id: KeyID, + remaining_budget: Credits, + required_budget: Credits, + #[source] + source_error: Box, }, - // ────────────────────────────────────────────────────────────────────────── - // Withdrawal document parsing errors - // ────────────────────────────────────────────────────────────────────────── - /// A withdrawal document could not be fully read (missing timestamp, invalid status, etc.). + /// A contract-bound key signed something outside its contract bounds. #[error( - "Could not read the withdrawal details. The data may be incomplete or in an unexpected format. Please retry." + "The key used to sign this action is limited to other contracts or documents, so it cannot sign this one. Choose a different key, then try again." )] - WithdrawalDocumentParsingError( - #[from] crate::backend_task::platform_info::WithdrawalParseError, - ), + SigningKeyOutOfContractBounds { + key_id: KeyID, + #[source] + source_error: Box, + }, - // ────────────────────────────────────────────────────────────────────────── - // SDK / RPC setup errors - // ────────────────────────────────────────────────────────────────────────── - /// The Dash Platform SDK could not be initialised with the current config, - /// or a context provider could not be bound to the current AppContext. + /// A contract-bound key signed an action that is not a document or token + /// operation. #[error( - "Could not connect to the Dash network. Please check your network settings and restart the application." + "The key used to sign this action is limited to one contract, so it cannot sign this kind of action. Choose a key that is not limited to a contract, then try again." )] - SdkInitializationFailed { detail: String }, - - /// The Core wallet name supplied by the user is syntactically invalid. - #[error("The Core wallet name '{name}' is invalid. Please check your wallet configuration.")] - InvalidCoreWalletName { name: String }, + SigningKeyContractBoundForNonBatch { + key_id: KeyID, + #[source] + source_error: Box, + }, - /// Dash Core has no wallets loaded — required for wallet-scoped RPC calls. - #[error("No wallets are loaded in Dash Core. Please open a wallet in Dash Core and retry.")] - NoCoreWalletsLoaded, + /// Key limits were requested that do not pass validation. + #[error(transparent)] + InvalidKeyLimits { + #[from] + source: crate::model::identity_key_limits::KeyLimitsError, + }, - // ────────────────────────────────────────────────────────────────────────── - // Wallet key / address errors - // ────────────────────────────────────────────────────────────────────────── - /// A private key for a wallet address could not be found or derived. + /// An identity this device holds is not on the network (e.g. it was + /// never registered, or the network was reset). #[error( - "Could not find the key for this address in your wallet. Please check your wallet and retry." + "This identity could not be found on the network. Refresh the identity, then try again." )] - WalletKeyLookupFailed, + IdentityMissingOnNetwork { identity_id: Identifier }, - /// A wallet-key derivation was requested at the empty derivation path. The - /// empty path IS the BIP-32 root, so deriving there yields the wallet's - /// master key rather than an address key — refused at the chokepoint every - /// key-bearing wallet task shares, never at an individual caller. + /// The key to change is not among the identity's keys on the network. #[error( - "This address has no known derivation path, so its private key cannot be shown or used. Choose an address with a known derivation path." + "This key is no longer part of the identity on the network. Refresh the identity to see its current keys." )] - RootKeyDerivationRefused, + IdentityKeyNotFound { key_id: KeyID }, - /// A wallet address or identity-auth key could not be derived. The upstream - /// detail (a legacy `String`) is logged at the call site, never stored here. - #[error("Could not generate a wallet address. Please check your wallet and retry.")] - WalletAddressDerivationFailed, + /// Key limits were requested on a network whose protocol version does not + /// support them yet. + #[error( + "This network does not support spending limits or expiry for keys yet. Add the key without limits, or try again after the network upgrades." + )] + KeyLimitsNotSupported, - /// A new Platform (DIP-17/18) receive address could not be derived or - /// registered. The underlying detail is logged, never shown to the user. - #[error("Could not generate a Platform receive address. Please check your wallet and retry.")] - WalletPlatformReceiveAddressFailed, + /// Remaining key budgets were requested from a network whose protocol + /// version does not track them yet. + #[error( + "This network does not track key spending limits yet. Try again after the network upgrades." + )] + KeyRemainingBudgetsNotSupported, - /// Signing a message with a wallet-derived key failed during derivation or - /// signing. The underlying detail is logged, never shown to the user. - #[error("Could not sign the message. Please check your wallet and retry.")] - WalletMessageSigningFailed, + /// An authentication key bound to a contract was requested on a network + /// that does not accept one yet. + #[error( + "This network does not support limiting an authentication key to one contract yet. Add the key without contract bounds, or try again after the network upgrades." + )] + ContractBoundAuthenticationKeysNotSupported, - /// The selected key type cannot be used to sign a message in this tool. - #[error("This key type cannot sign a message. Please choose an ECDSA key and try again.")] - WalletMessageSignUnsupportedKeyType, + /// No key this device holds may sign a key limits update: that takes a + /// MASTER key, or a CRITICAL authentication key with no limits and no + /// contract bounds. + #[error( + "Changing key limits needs this identity's master key, or a critical key without limits. Import one of them on this device, then try again." + )] + NoKeyLimitsSigningKey, - // ────────────────────────────────────────────────────────────────────────── - // Payment errors - // ────────────────────────────────────────────────────────────────────────── - /// A recipient address could not be parsed or is invalid. - #[error("The recipient address '{address}' is not valid. Please check the address and retry.")] - InvalidRecipientAddress { - address: String, + /// A key limits update was accepted on the network, but storing the + /// updated key on this device afterward failed. The broadcast and the + /// local write cannot be atomic; this reports the change as made rather + /// than as failed, so the user does not retry a change already applied. + #[error( + "The key's new limits are set on the network, but they could not be saved on this device. Refresh the identity to load them." + )] + KeyLimitsRaisedButNotSaved { #[source] - source: dashcore::address::Error, + source: Box, }, - /// A recipient address was parsed but does not match the current network. + /// A key limits update was accepted on the network and the updated key + /// is stored on this device, but the identity's balance after the fee + /// could not be read. The stored balance is the one before the update. + /// `source` is `None` when Platform reported no balance for the identity. #[error( - "The address does not match the current network. Please check that you are on the correct network." + "The key's new limits are set, but the identity's new balance could not be read. Refresh the identity to see its balance." )] - AddressNetworkMismatch { + KeyLimitsRaisedButBalanceNotRefreshed { #[source] - source: dashcore::address::Error, + source: Option>, }, - /// The payment had no recipients. A transaction must pay at least one - /// address, so the request is rejected before any funds move. - #[error("Add at least one recipient before sending a payment.")] - PaymentNoRecipients, - - /// Tracked-lock funding was invoked with no recipients. The UI always - /// supplies at least one, so this is a caller-contract violation (a future - /// programmatic caller, not a normal user action) rejected before any - /// pool-membership lookup or broadcast. - #[error("No funding recipients were provided for this asset lock.")] - NoFundingRecipients, - - /// A recipient was given a zero amount. Sending nothing wastes the network - /// fee and is almost always a slip, so it is rejected up front. - #[error("Enter an amount greater than zero for every recipient, then try again.")] - PaymentZeroAmount, - - /// The wallet has no UTXOs available to cover the payment. - #[error("Your wallet has no available funds to spend. Please receive some Dash first.")] - NoUtxosAvailable, - - /// The wallet balance is too low to cover the requested amount plus fees. + /// Platform refused a key whose expiry is not in the future. #[error( - "You do not have enough Dash. You have {available} duffs but need {required} duffs. Please add more funds and retry." + "The key's expiry is already in the past, so the network refused the change. Choose a later expiry, then try again." )] - InsufficientFunds { available: u64, required: u64 }, + KeyExpiryInPast { + key_id: KeyID, + #[source] + source_error: Box, + }, - /// The output amount is smaller than the transaction fee. + /// Platform refused a limits update that does not raise the stored limit, + /// typically because the key changed since it was loaded. #[error( - "The amount is too small to cover the {fee} duff transaction fee. Please send a larger amount." + "The key's limits have changed since they were loaded, so the new value would not raise them. Refresh the identity, then try again." )] - OutputTooSmallForFee { fee: u64 }, + KeyLimitNotRaised { + key_id: KeyID, + #[source] + source_error: Box, + }, - /// A signature hash for a transaction input could not be computed. - #[error("Could not sign the transaction. Please retry.")] - SighashComputationFailed { + /// Platform refused raising a limit the key does not have. + #[error( + "This key does not have that limit, and a limit can only be raised, not added. Refresh the identity to see the key's current limits." + )] + KeyLimitNotSet { + key_id: KeyID, #[source] - source: dashcore::sighash::Error, + source_error: Box, }, - /// Could not access wallet information from the SPV manager. - #[error("Your wallet is still loading. Please wait a moment and try again.")] - WalletInfoUnavailable, + /// A key with limits was used to sign a key limits update. + #[error( + "A key with its own spending limit or expiry cannot change key limits. Use the identity's master key instead, then try again." + )] + KeyLimitsSignerHasLimits { + key_id: KeyID, + #[source] + source_error: Box, + }, - /// Expected BIP44 account not found at the given index. - #[error("Your wallet needs to be refreshed before sending. Please refresh and try again.")] - MissingBip44Account { index: u32 }, + /// Platform refused limits on a key that may not carry them, a zero + /// budget, or a limits update that changes nothing. + #[error( + "The network refused these key limits. Only authentication keys below the master level can have them, and a spending limit must be above zero. Adjust the limits, then try again." + )] + KeyLimitsRefused { + key_id: KeyID, + #[source] + source_error: Box, + }, - /// Could not derive a change address from the wallet account. - #[error("Could not prepare the transaction. Please refresh your wallet and try again.")] - ChangeAddressDerivation { + /// The key to change is disabled. + #[error("This key is disabled, so it cannot be used or changed. Choose a different key.")] + IdentityKeyDisabled { + key_id: KeyID, #[source] - source: dash_sdk::dpp::key_wallet::Error, + source_error: Box, }, // ────────────────────────────────────────────────────────────────────────── - // Token query errors (identity / recipient validation) + // Document action fees, moderation and gas sponsorship (protocol version 14) // ────────────────────────────────────────────────────────────────────────── - /// No local identities are registered — a prerequisite for token queries. - #[error("No registered identities found. Please register an identity first.")] - NoIdentitiesFound, + /// A document action's fee agreement is missing or names another fee + /// than the document type declares; refused before anything is signed. + #[error(transparent)] + ActionFeeAgreement { + #[from] + source: crate::model::fee_estimation::ActionFeeAgreementError, + }, - /// The current identity is not the contract owner who can claim this token's distribution. + /// The document type charges an action fee and the transition carried no + /// agreement to it. #[error( - "This token distribution can only be claimed by the contract owner ({contract_owner}). Your identity is not the contract owner." + "This contract charges a fee for this action, and the request did not include your agreement to it. Try again and confirm the contract fee when asked." )] - NotContractOwner { contract_owner: String }, + DocumentActionFeeNotAgreed { + #[source] + source_error: Box, + }, - /// The current identity is not the specific identity designated as the token distribution recipient. + /// The fee the transition agreed to differs from the one the document + /// type declares, typically because the contract changed meanwhile. #[error( - "This token distribution can only be claimed by the designated recipient ({designated_recipient}). Your identity is not the designated recipient." + "The contract fee for this action is not the one you agreed to, because the contract changed. Reload the contract to see its current fee, then try again." )] - NotDesignatedTokenRecipient { designated_recipient: String }, + DocumentActionFeeChanged { + #[source] + source_error: Box, + }, - /// The current identity is not an evonode, which is required for this token distribution. + /// Network fees rose past the increase the fee agreement tolerates. + /// Reported only after a proved epoch refresh updated the cached fee + /// multiplier; the node-reported multipliers here are unproven and are + /// never adopted (see `DocumentActionFeeMultiplierNotRefreshed`). #[error( - "This token distribution is only for evonode identities. Your identity is not registered as an evonode." + "Network fees rose by more than {increase_tolerance_percent}% since you confirmed the contract fee, so the network refused the action. The contract fee has been updated to the current network fees. Try again to see and confirm the new amount." )] - NotEvonode, + DocumentActionFeeMultiplierRose { + known_fee_multiplier_permille: u64, + current_fee_multiplier_permille: u64, + increase_tolerance_percent: u16, + #[source] + source_error: Box, + }, - // ────────────────────────────────────────────────────────────────────────── - // Wallet-based identity loading errors - // ────────────────────────────────────────────────────────────────────────── - /// No on-chain identity was found for the requested wallet derivation index. + /// A document action was refused because network fees rose, and the + /// proved refresh of the current fees failed, so the cached fee + /// multiplier (and the next quote) keeps its previous value. #[error( - "Could not find an identity for wallet index {identity_index} after checking {auth_key_count} keys. Try expanding the search range." + "Network fees rose by more than {increase_tolerance_percent}% since you confirmed the contract fee, so the network refused the action. The current network fees could not be loaded, so the contract fee could not be updated. Check your connection, wait a moment, and then try again." )] - WalletIdentityNotFound { - identity_index: u32, - auth_key_count: usize, + DocumentActionFeeMultiplierNotRefreshed { + increase_tolerance_percent: u16, + /// Why the proved epoch refresh failed. + #[source] + source_error: Box, }, - /// The identity returned by the platform does not contain the queried authentication key. + /// The identity is banned on the contract. #[error( - "The identity retrieved does not match your wallet key. Please check you are using the correct wallet." + "This identity is banned from contract {} and cannot act on its documents. Use a different identity for this contract.", + contract_id.to_string(Encoding::Base58) )] - WalletIdentityKeyMismatch, + ContractUserBanned { + contract_id: Identifier, + identity_id: Identifier, + #[source] + source_error: Box, + }, - /// None of the identity's public keys could be matched to wallet derivation paths. + /// The identity is suspended on the contract until a given time. #[error( - "Could not match any identity keys to your wallet. Please check your wallet and retry." + "This identity is suspended from contract {} until {}. Wait until then, or use a different identity for this contract.", + contract_id.to_string(Encoding::Base58), + crate::model::datetime::local_date_time_from_unix_millis(*until_ms) )] - NoMatchingWalletKeys, + ContractUserSuspended { + contract_id: Identifier, + identity_id: Identifier, + /// End of the suspension, in milliseconds since the Unix epoch. + until_ms: u64, + #[source] + source_error: Box, + }, - /// The derivation path for the queried identity key was not found in the wallet. + /// The contract owner sponsoring the gas cannot cover it. + #[error( + "The contract owner pays the network fee for this action but does not have enough credits for it right now. Try again later." + )] + GasSponsorInsufficientBalance { + sponsor_id: Identifier, + balance: Credits, + required_balance: Credits, + #[source] + source_error: Box, + }, + + /// The transition asked for a gas payer the document type does not offer. + #[error( + "This document type does not let that party pay the network fee for this action. Pay the fee as the document type allows, then try again." + )] + GasFeesPaidByNotAllowed { + #[source] + source_error: Box, + }, + + /// The transitions of one batch named different gas payers. + #[error( + "The actions in this request name different payers for the network fee. Send them one at a time, then try again." + )] + InconsistentGasFeesPayerInBatch { + #[source] + source_error: Box, + }, + + // ────────────────────────────────────────────────────────────────────────── + // Username, vote and token transfer refusals (protocol versions 14 and 15) + // ────────────────────────────────────────────────────────────────────────── + /// The contest for the requested name already holds the most contenders it + /// accepts. + #[error( + "Too many people have already asked for this username, so no one else can join. Choose a different username." + )] + DocumentContestContendersLimitReached { + max_contenders: u16, + #[source] + source_error: Box, + }, + + /// The token cannot be moved to another identity, so a transfer, or a + /// payment that moves it, is refused. + #[error( + "Token {} cannot be moved to another identity, so this action is not possible. Use a different token.", + token_id.to_string(Encoding::Base58) + )] + TokenNotTransferable { + token_id: Identifier, + #[source] + source_error: Box, + }, + + /// The vote choice is not allowed for this contest: a lock on a name that + /// cannot be locked, or a vote towards an identity that is not competing. + #[error( + "This vote is not allowed for this username. Vote for one of the identities that asked for it, or choose another option, then vote again." + )] + VoteChoiceNotAllowed { + #[source] + source_error: Box, + }, + + /// Another identity committed to the same name earlier, so its request + /// comes first. + #[error("Someone else asked for this username first. Choose a different username.")] + ReferencedDocumentPreceded { + document_id: Identifier, + preceding_document_id: Identifier, + #[source] + source_error: Box, + }, + + /// A token that cannot be transferred cannot be tradeable either. + #[error( + "The token at position {token_position} cannot be traded because it cannot be transferred. Allow transfers or turn trading off, then try again." + )] + NonTransferableTokenTradeable { + token_position: u16, + #[source] + source_error: Box, + }, + + // ────────────────────────────────────────────────────────────────────────── + // Contract fee pots (protocol version 14) + // ────────────────────────────────────────────────────────────────────────── + /// The network does not keep contract fee pots yet. + #[error( + "This network does not support contract fees yet. Try again after the network upgrades." + )] + ContractFeePotsNotSupported, + + /// The claiming identity has no key allowed to sign a fee claim. + #[error( + "Claiming contract fees needs a critical authentication key that is not limited to a contract. Add such a key to this identity, then try again." + )] + NoFeeClaimSigningKey, + + /// A fee claim was paid out on the network, but storing the claimant's + /// new balance on this device afterward failed. Reported as paid out + /// rather than as failed, so the user does not retry a claim already made. + #[error( + "The fees were paid out, but the identity's new balance could not be saved on this device. Refresh the identity to see its new balance." + )] + ContractFeesClaimedButBalanceNotSaved { + #[source] + source: Box, + }, + + /// The claiming identity is not a recipient of the pot. + #[error( + "Only the contract owner can claim the owner's fees, and only a moderator can claim the moderators' fees. Choose an identity that receives these fees, then try again." + )] + ContractFeeClaimNotAllowed { + #[source] + source_error: Box, + }, + + /// The pot was already paid out in the current epoch. + #[error( + "These fees were already claimed in the current epoch. Fees can be claimed once per epoch, so try again in the next one." + )] + ContractFeesAlreadyClaimedThisEpoch { + epoch_index: u16, + #[source] + source_error: Box, + }, + + /// The pot holds nothing to pay out. + #[error("There are no fees to claim yet. Try again after users have paid fees to this contract.")] + ContractFeesNothingToClaim { + #[source] + source_error: Box, + }, + + // ────────────────────────────────────────────────────────────────────────── + // Contract authoring (protocol version 14) + // ────────────────────────────────────────────────────────────────────────── + /// A replace changed a property the document type declares immutable. + #[error( + "One of the fields you changed cannot be changed after the document is created. Keep the original values of its fixed fields, then try again." + )] + DocumentImmutablePropertyChanged { + // The node's names for the document type and the property stay in + // `source_error` only: unproven text, never copied into the variant. + document_id: Identifier, + #[source] + source_error: Box, + }, + + /// A `deletableDocument` reference points at a document type whose + /// documents cannot be deleted. + #[error( + "This contract refers to documents that can never be deleted as if they could be. Make that reference a permanent document reference, then try again." + )] + ReferencedDocumentTypeNotDeletable { + // The node's names for the document type and the reference path stay + // in `source_error` only: unproven text, never copied into the variant. + contract_id: Identifier, + #[source] + source_error: Box, + }, + + /// A document type charges moderators a fee the contract has no + /// moderation for. + #[error( + "A document type in this contract charges a fee for moderators, but the contract has no moderation. Remove the moderators' part of the fee, then try again." + )] + DocumentActionFeesWithoutModeration { + // The node's name for the document type stays in `source_error` only: + // unproven text, never copied into the variant. + #[source] + source_error: Box, + }, + + /// A once-per-identity distribution amount is zero or above the maximum. + #[error( + "The amount each identity can claim must be between 1 and {max_amount}. Enter an amount in that range, then try again." + )] + InvalidOncePerIdentityDistributionAmount { + amount: u64, + max_amount: u64, + #[source] + source_error: Box, + }, + + /// The pre-programmed distributions of one time add up to more than the + /// maximum. + #[error( + "The scheduled token distributions at {} add up to more than the allowed maximum. Lower those amounts, then try again.", + crate::model::datetime::local_date_time_from_unix_millis(*timestamp_ms) + )] + PreProgrammedDistributionOverLimit { + token_position: u16, + /// The distribution time, in milliseconds since the Unix epoch. + timestamp_ms: u64, + #[source] + source_error: Box, + }, + + // ────────────────────────────────────────────────────────────────────────── + // Token query errors + // ────────────────────────────────────────────────────────────────────────── + #[error("Shielded token settings cannot be changed here yet. Use a supported token setting instead.")] + TokenShieldedConfigurationUnsupported, + + /// Querying token data from the platform failed. + #[error("Could not retrieve token information from the platform. Please retry.")] + TokenQueryError { detail: String }, + + /// A token with a once-per-identity distribution was requested on a + /// network that does not accept one yet. + #[error( + "This network does not support once-per-identity token distributions yet. Create the token without it, or try again after the network upgrades." + )] + TokenOncePerIdentityNotSupported, + + /// The token creator asked for a token that cannot be transferred, on a + /// network that does not accept one yet. + #[error( + "The connected network does not accept tokens that cannot be transferred yet. Allow transfers, or try again after the network upgrades." + )] + NonTransferableTokenNotSupported, + + /// A once-per-identity token claim succeeded, but this device could not + /// record that it was made. Without the record, the claim may be offered + /// again, and Platform refuses (and charges for) a second claim. + #[error( + "The tokens were claimed, but this device could not save that this identity has claimed them. Do not claim this token again with this identity, because the claim would be refused." + )] + TokensClaimedButClaimNotRecorded { + #[source] + source: Box, + }, + + /// The identity already took its single claim of a token's + /// once-per-identity distribution, so Platform refused another one. + #[error( + "This identity has already claimed its share of this token. Each identity can claim it only once, so there is nothing more to claim." + )] + TokenOncePerIdentityAlreadyClaimed { + token_id: Identifier, + identity_id: Identifier, + /// Block time of the claim that was paid, in milliseconds. + claimed_at_ms: u64, + #[source] + source_error: Box, + }, + + /// A token operation run by the wallet runtime failed for a reason the + /// SDK error classification has no specific variant for. Built by + /// [`TaskError::from_token_operation_failure`], which prefers the specific + /// variant whenever there is one. + #[error("{}", operation.failure_message())] + TokenOperationFailed { + operation: TokenOperationKind, + #[source] + source_error: Box, + }, + + /// The token does not have a perpetual distribution configured — no rewards to claim. + #[error("This token does not have perpetual distribution, so there are no rewards to claim.")] + TokenNoPerpetualDistribution, + + /// The recipient identity does not exist on Platform (e.g. during a token mint). + #[error( + "The recipient identity `{recipient_id}` does not exist on the platform. \ + Check the ID and try again, or create the identity first." + )] + TokenRecipientIdentityNotFound { + recipient_id: String, + #[source] + source_error: Box, + }, + + /// The identity's token account is not frozen, so an unfreeze / destroy-frozen action + /// cannot proceed. + #[error( + "Identity `{identity_id}` is not frozen for token `{token_id}`, so `{action}` cannot proceed. \ + Refresh the frozen-account list and try again." + )] + TokenAccountNotFrozen { + identity_id: String, + token_id: String, + action: String, + #[source] + source_error: Box, + }, + + // ────────────────────────────────────────────────────────────────────────── + // Contract schema errors + // ────────────────────────────────────────────────────────────────────────── + /// The contract structure does not match expectations (e.g. missing contested index). + #[error("The contract structure is unexpected. Please update the application.")] + ContractSchemaMismatch { detail: &'static str }, + + /// Platform returned a different contract after accepting an update. + #[error( + "The platform returned contract {returned} instead of {expected}. Please try the update again." + )] + UpdatedContractIdMismatch { + expected: Identifier, + returned: Identifier, + }, + + // ────────────────────────────────────────────────────────────────────────── + // Withdrawal document parsing errors + // ────────────────────────────────────────────────────────────────────────── + /// A withdrawal document could not be fully read (missing timestamp, invalid status, etc.). + #[error( + "Could not read the withdrawal details. The data may be incomplete or in an unexpected format. Please retry." + )] + WithdrawalDocumentParsingError( + #[from] crate::backend_task::platform_info::WithdrawalParseError, + ), + + // ────────────────────────────────────────────────────────────────────────── + // SDK / RPC setup errors + // ────────────────────────────────────────────────────────────────────────── + /// The Dash Platform SDK could not be initialised with the current config, + /// or a context provider could not be bound to the current AppContext. + #[error( + "Could not connect to the Dash network. Please check your network settings and restart the application." + )] + SdkInitializationFailed { detail: String }, + + /// The Core wallet name supplied by the user is syntactically invalid. + #[error("The Core wallet name '{name}' is invalid. Please check your wallet configuration.")] + InvalidCoreWalletName { name: String }, + + /// Dash Core has no wallets loaded — required for wallet-scoped RPC calls. + #[error("No wallets are loaded in Dash Core. Please open a wallet in Dash Core and retry.")] + NoCoreWalletsLoaded, + + // ────────────────────────────────────────────────────────────────────────── + // Wallet key / address errors + // ────────────────────────────────────────────────────────────────────────── + /// A private key for a wallet address could not be found or derived. + #[error( + "Could not find the key for this address in your wallet. Please check your wallet and retry." + )] + WalletKeyLookupFailed, + + /// A wallet-key derivation was requested at the empty derivation path. The + /// empty path IS the BIP-32 root, so deriving there yields the wallet's + /// master key rather than an address key — refused at the chokepoint every + /// key-bearing wallet task shares, never at an individual caller. + #[error( + "This address has no known derivation path, so its private key cannot be shown or used. Choose an address with a known derivation path." + )] + RootKeyDerivationRefused, + + /// A wallet address or identity-auth key could not be derived. The upstream + /// detail (a legacy `String`) is logged at the call site, never stored here. + #[error("Could not generate a wallet address. Please check your wallet and retry.")] + WalletAddressDerivationFailed, + + /// A new Platform (DIP-17/18) receive address could not be derived or + /// registered. The underlying detail is logged, never shown to the user. + #[error("Could not generate a Platform receive address. Please check your wallet and retry.")] + WalletPlatformReceiveAddressFailed, + + /// Signing a message with a wallet-derived key failed during derivation or + /// signing. The underlying detail is logged, never shown to the user. + #[error("Could not sign the message. Please check your wallet and retry.")] + WalletMessageSigningFailed, + + /// The selected key type cannot be used to sign a message in this tool. + #[error("This key type cannot sign a message. Please choose an ECDSA key and try again.")] + WalletMessageSignUnsupportedKeyType, + + // ────────────────────────────────────────────────────────────────────────── + // Payment errors + // ────────────────────────────────────────────────────────────────────────── + /// A recipient address could not be parsed or is invalid. + #[error("The recipient address '{address}' is not valid. Please check the address and retry.")] + InvalidRecipientAddress { + address: String, + #[source] + source: dashcore::address::Error, + }, + + /// A recipient address was parsed but does not match the current network. + #[error( + "The address does not match the current network. Please check that you are on the correct network." + )] + AddressNetworkMismatch { + #[source] + source: dashcore::address::Error, + }, + + /// The payment had no recipients. A transaction must pay at least one + /// address, so the request is rejected before any funds move. + #[error("Add at least one recipient before sending a payment.")] + PaymentNoRecipients, + + /// Tracked-lock funding was invoked with no recipients. The UI always + /// supplies at least one, so this is a caller-contract violation (a future + /// programmatic caller, not a normal user action) rejected before any + /// pool-membership lookup or broadcast. + #[error("No funding recipients were provided for this asset lock.")] + NoFundingRecipients, + + /// A recipient was given a zero amount. Sending nothing wastes the network + /// fee and is almost always a slip, so it is rejected up front. + #[error("Enter an amount greater than zero for every recipient, then try again.")] + PaymentZeroAmount, + + /// The wallet has no UTXOs available to cover the payment. + #[error("Your wallet has no available funds to spend. Please receive some Dash first.")] + NoUtxosAvailable, + + /// The wallet balance is too low to cover the requested amount plus fees. + #[error( + "You do not have enough Dash. You have {available} duffs but need {required} duffs. Please add more funds and retry." + )] + InsufficientFunds { available: u64, required: u64 }, + + /// The output amount is smaller than the transaction fee. + #[error( + "The amount is too small to cover the {fee} duff transaction fee. Please send a larger amount." + )] + OutputTooSmallForFee { fee: u64 }, + + /// A signature hash for a transaction input could not be computed. + #[error("Could not sign the transaction. Please retry.")] + SighashComputationFailed { + #[source] + source: dashcore::sighash::Error, + }, + + /// Could not access wallet information from the SPV manager. + #[error("Your wallet is still loading. Please wait a moment and try again.")] + WalletInfoUnavailable, + + /// Expected BIP44 account not found at the given index. + #[error("Your wallet needs to be refreshed before sending. Please refresh and try again.")] + MissingBip44Account { index: u32 }, + + /// Could not derive a change address from the wallet account. + #[error("Could not prepare the transaction. Please refresh your wallet and try again.")] + ChangeAddressDerivation { + #[source] + source: dash_sdk::dpp::key_wallet::Error, + }, + + // ────────────────────────────────────────────────────────────────────────── + // Token query errors (identity / recipient validation) + // ────────────────────────────────────────────────────────────────────────── + /// No local identities are registered — a prerequisite for token queries. + #[error("No registered identities found. Please register an identity first.")] + NoIdentitiesFound, + + /// The current identity is not the contract owner who can claim this token's distribution. + #[error( + "This token distribution can only be claimed by the contract owner ({contract_owner}). Your identity is not the contract owner." + )] + NotContractOwner { contract_owner: String }, + + /// The current identity is not the specific identity designated as the token distribution recipient. + #[error( + "This token distribution can only be claimed by the designated recipient ({designated_recipient}). Your identity is not the designated recipient." + )] + NotDesignatedTokenRecipient { designated_recipient: String }, + + /// The current identity is not an evonode, which is required for this token distribution. + #[error( + "This token distribution is only for evonode identities. Your identity is not registered as an evonode." + )] + NotEvonode, + + // ────────────────────────────────────────────────────────────────────────── + // Wallet-based identity loading errors + // ────────────────────────────────────────────────────────────────────────── + /// No on-chain identity was found for the requested wallet derivation index. + #[error( + "Could not find an identity for wallet index {identity_index} after checking {auth_key_count} keys. Try expanding the search range." + )] + WalletIdentityNotFound { + identity_index: u32, + auth_key_count: usize, + }, + + /// The identity returned by the platform does not contain the queried authentication key. + #[error( + "The identity retrieved does not match your wallet key. Please check you are using the correct wallet." + )] + WalletIdentityKeyMismatch, + + /// None of the identity's public keys could be matched to wallet derivation paths. + #[error( + "Could not match any identity keys to your wallet. Please check your wallet and retry." + )] + NoMatchingWalletKeys, + + /// The derivation path for the queried identity key was not found in the wallet. #[error( "Could not locate this identity key's information in your wallet. Please check your wallet configuration." )] @@ -3828,6 +4468,26 @@ impl From for TaskError { } } +impl TaskError { + /// The error for a token operation the wallet runtime reported as failed + /// (`PlatformWalletError::TokenOperationFailed`). + /// + /// The SDK source is classified exactly like any other SDK error, so a + /// rejection with a specific meaning (insufficient balance, a claim already + /// taken, …) keeps its specific message. Only an otherwise unclassified + /// failure becomes [`TaskError::TokenOperationFailed`], which at least names + /// the operation. + pub fn from_token_operation_failure(operation: &'static str, source: SdkError) -> Self { + match TaskError::from(source) { + TaskError::SdkError { source_error } => TaskError::TokenOperationFailed { + operation: TokenOperationKind::from_upstream_label(operation), + source_error, + }, + classified => classified, + } + } +} + /// Why the DET identity signer could not resolve a signing key. /// /// The upstream `Signer` trait can only return `ProtocolError`, which has no @@ -4133,11 +4793,346 @@ impl From for TaskError { } })) } + ConsensusError::StateError( + StateError::TokenOncePerIdentityDistributionAlreadyClaimedError(e), + ) => { + let (token_id, identity_id, claimed_at_ms) = + (e.token_id(), e.identity_id(), e.claimed_at_ms()); + Some(Box::new(move |source_error| { + TaskError::TokenOncePerIdentityAlreadyClaimed { + token_id, + identity_id, + claimed_at_ms, + source_error, + } + })) + } ConsensusError::StateError(StateError::DuplicateUniqueIndexError(_)) => { Some(Box::new(|source_error| TaskError::PlatformEntryConflict { source_error, })) } + ConsensusError::SignatureError(SignatureError::PublicKeyExpiredError(e)) => { + let (key_id, expired_at_ms) = (e.public_key_id(), e.expires_at()); + Some(Box::new(move |source_error| TaskError::SigningKeyExpired { + key_id, + expired_at_ms, + source_error, + })) + } + ConsensusError::SignatureError( + SignatureError::PublicKeyBudgetExhaustedError(e), + ) => { + let key_id = e.public_key_id(); + Some(Box::new(move |source_error| { + TaskError::SigningKeyBudgetExhausted { + key_id, + source_error, + } + })) + } + ConsensusError::StateError( + StateError::IdentityPublicKeyBudgetExceededError(e), + ) => { + let (key_id, remaining_budget, required_budget) = + (e.public_key_id(), e.remaining_budget(), e.required_budget()); + Some(Box::new(move |source_error| { + TaskError::SigningKeyBudgetExceeded { + key_id, + remaining_budget, + required_budget, + source_error, + } + })) + } + ConsensusError::SignatureError( + SignatureError::ContractBoundedKeyOutOfBoundsError(e), + ) => { + let key_id = *e.public_key_id(); + Some(Box::new(move |source_error| { + TaskError::SigningKeyOutOfContractBounds { + key_id, + source_error, + } + })) + } + ConsensusError::SignatureError( + SignatureError::ContractBoundedKeyNonBatchError(e), + ) => { + let key_id = *e.public_key_id(); + Some(Box::new(move |source_error| { + TaskError::SigningKeyContractBoundForNonBatch { + key_id, + source_error, + } + })) + } + ConsensusError::StateError(StateError::ContractFeeClaimNotAllowedError(_)) => { + Some(Box::new(|source_error| { + TaskError::ContractFeeClaimNotAllowed { source_error } + })) + } + ConsensusError::StateError( + StateError::ContractFeesAlreadyClaimedThisEpochError(e), + ) => { + let epoch_index = e.epoch_index(); + Some(Box::new(move |source_error| { + TaskError::ContractFeesAlreadyClaimedThisEpoch { + epoch_index, + source_error, + } + })) + } + ConsensusError::StateError(StateError::ContractFeesNothingToClaimError(_)) => { + Some(Box::new(|source_error| { + TaskError::ContractFeesNothingToClaim { source_error } + })) + } + ConsensusError::StateError( + StateError::IdentityPublicKeyAlreadyExpiredError(e), + ) => { + let key_id = e.public_key_id(); + Some(Box::new(move |source_error| TaskError::KeyExpiryInPast { + key_id, + source_error, + })) + } + ConsensusError::StateError( + StateError::IdentityPublicKeyLimitNotRaisedError(e), + ) => { + let key_id = e.public_key_id(); + Some(Box::new(move |source_error| TaskError::KeyLimitNotRaised { + key_id, + source_error, + })) + } + ConsensusError::StateError(StateError::IdentityPublicKeyLimitNotSetError( + e, + )) => { + let key_id = e.public_key_id(); + Some(Box::new(move |source_error| TaskError::KeyLimitNotSet { + key_id, + source_error, + })) + } + ConsensusError::SignatureError( + SignatureError::PublicKeyWithLimitsCannotUpdateKeyLimitsError(e), + ) => { + let key_id = e.public_key_id(); + Some(Box::new(move |source_error| { + TaskError::KeyLimitsSignerHasLimits { + key_id, + source_error, + } + })) + } + ConsensusError::BasicError( + BasicError::IdentityPublicKeyLimitsNotAllowedError(e), + ) => { + let key_id = e.public_key_id(); + Some(Box::new(move |source_error| TaskError::KeyLimitsRefused { + key_id, + source_error, + })) + } + ConsensusError::BasicError( + BasicError::InvalidIdentityPublicKeyBudgetError(e), + ) => { + let key_id = e.public_key_id(); + Some(Box::new(move |source_error| TaskError::KeyLimitsRefused { + key_id, + source_error, + })) + } + ConsensusError::BasicError(BasicError::IdentityKeyLimitsUpdateEmptyError( + e, + )) => { + let key_id = e.public_key_id(); + Some(Box::new(move |source_error| TaskError::KeyLimitsRefused { + key_id, + source_error, + })) + } + ConsensusError::StateError(StateError::IdentityPublicKeyIsDisabledError(e)) => { + let key_id = e.public_key_index(); + Some(Box::new(move |source_error| { + TaskError::IdentityKeyDisabled { + key_id, + source_error, + } + })) + } + ConsensusError::StateError( + StateError::DocumentActionFeeAgreementNotSetError(_), + ) => Some(Box::new(|source_error| { + TaskError::DocumentActionFeeNotAgreed { source_error } + })), + ConsensusError::StateError( + StateError::DocumentActionFeeAgreementMismatchError(_), + ) => Some(Box::new(|source_error| { + TaskError::DocumentActionFeeChanged { source_error } + })), + ConsensusError::StateError( + StateError::DocumentActionFeeMultiplierNotToleratedError(e), + ) => { + let (known, current, tolerance) = ( + e.known_fee_multiplier_permille(), + e.current_fee_multiplier_permille(), + e.increase_tolerance_percent(), + ); + Some(Box::new(move |source_error| { + TaskError::DocumentActionFeeMultiplierRose { + known_fee_multiplier_permille: known, + current_fee_multiplier_permille: current, + increase_tolerance_percent: tolerance, + source_error, + } + })) + } + ConsensusError::StateError(StateError::ContractUserBannedError(e)) => { + let (contract_id, identity_id) = (e.contract_id(), e.identity_id()); + Some(Box::new(move |source_error| { + TaskError::ContractUserBanned { + contract_id, + identity_id, + source_error, + } + })) + } + ConsensusError::StateError(StateError::ContractUserSuspendedError(e)) => { + let (contract_id, identity_id, until_ms) = + (e.contract_id(), e.identity_id(), e.until()); + Some(Box::new(move |source_error| { + TaskError::ContractUserSuspended { + contract_id, + identity_id, + until_ms, + source_error, + } + })) + } + ConsensusError::StateError(StateError::GasSponsorInsufficientBalanceError( + e, + )) => { + let (sponsor_id, balance, required_balance) = + (*e.sponsor_id(), e.balance(), e.required_balance()); + Some(Box::new(move |source_error| { + TaskError::GasSponsorInsufficientBalance { + sponsor_id, + balance, + required_balance, + source_error, + } + })) + } + ConsensusError::StateError(StateError::GasFeesPaidByNotAllowedError(_)) => { + Some(Box::new(|source_error| { + TaskError::GasFeesPaidByNotAllowed { source_error } + })) + } + ConsensusError::StateError( + StateError::InconsistentGasFeesPaidByInBatchError(_), + ) => Some(Box::new(|source_error| { + TaskError::InconsistentGasFeesPayerInBatch { source_error } + })), + ConsensusError::StateError( + StateError::DocumentContestMaximumContendersReachedError(e), + ) => { + let max_contenders = e.max_contenders(); + Some(Box::new(move |source_error| { + TaskError::DocumentContestContendersLimitReached { + max_contenders, + source_error, + } + })) + } + ConsensusError::StateError(StateError::TokenNotTransferableError(e)) => { + let token_id = *e.token_id(); + Some(Box::new(move |source_error| { + TaskError::TokenNotTransferable { + token_id, + source_error, + } + })) + } + ConsensusError::StateError( + StateError::VoteChoiceNotAllowedForVotePollError(_), + ) => Some(Box::new(|source_error| TaskError::VoteChoiceNotAllowed { + source_error, + })), + ConsensusError::StateError(StateError::ReferencedDocumentPrecededError(e)) => { + let (document_id, preceding_document_id) = + (*e.document_id(), *e.preceding_document_id()); + Some(Box::new(move |source_error| { + TaskError::ReferencedDocumentPreceded { + document_id, + preceding_document_id, + source_error, + } + })) + } + ConsensusError::BasicError(BasicError::NonTransferableTokenTradeableError( + e, + )) => { + let token_position = e.token_contract_position(); + Some(Box::new(move |source_error| { + TaskError::NonTransferableTokenTradeable { + token_position, + source_error, + } + })) + } + ConsensusError::StateError( + StateError::DocumentImmutablePropertyChangedError(e), + ) => { + let document_id = e.document_id(); + Some(Box::new(move |source_error| { + TaskError::DocumentImmutablePropertyChanged { + document_id, + source_error, + } + })) + } + ConsensusError::StateError( + StateError::ReferencedDocumentTypeNotDeletableError(e), + ) => { + let contract_id = *e.contract_id(); + Some(Box::new(move |source_error| { + TaskError::ReferencedDocumentTypeNotDeletable { + contract_id, + source_error, + } + })) + } + ConsensusError::BasicError( + BasicError::DocumentActionFeesWithoutModerationError(_), + ) => Some(Box::new(|source_error| { + TaskError::DocumentActionFeesWithoutModeration { source_error } + })), + ConsensusError::BasicError( + BasicError::InvalidTokenOncePerIdentityDistributionAmountError(e), + ) => { + let (amount, max_amount) = (e.amount(), e.max_amount()); + Some(Box::new(move |source_error| { + TaskError::InvalidOncePerIdentityDistributionAmount { + amount, + max_amount, + source_error, + } + })) + } + ConsensusError::BasicError( + BasicError::PreProgrammedDistributionAmountOverLimitError(e), + ) => { + let (token_position, timestamp_ms) = (e.token_position(), e.timestamp()); + Some(Box::new(move |source_error| { + TaskError::PreProgrammedDistributionOverLimit { + token_position, + timestamp_ms, + source_error, + } + })) + } _ => None, } }); @@ -4301,10 +5296,12 @@ mod tests { InvalidTokenNameCharacterError, InvalidTokenNameLengthError, }; use dash_sdk::dpp::consensus::basic::identity::InvalidInstantAssetLockProofSignatureError; - use dash_sdk::dpp::consensus::state::identity::duplicated_identity_public_key_id_state_error::DuplicatedIdentityPublicKeyIdStateError; - use dash_sdk::dpp::consensus::state::identity::duplicated_identity_public_key_state_error::DuplicatedIdentityPublicKeyStateError; - use dash_sdk::dpp::consensus::state::identity::IdentityInsufficientBalanceError; - use dash_sdk::dpp::consensus::state::identity::identity_public_key_already_exists_for_unique_contract_bounds_error::IdentityPublicKeyAlreadyExistsForUniqueContractBoundsError; + use dash_sdk::dpp::consensus::state::identity::{ + IdentityInsufficientBalanceError, + duplicated_identity_public_key_id_state_error::DuplicatedIdentityPublicKeyIdStateError, + duplicated_identity_public_key_state_error::DuplicatedIdentityPublicKeyStateError, + identity_public_key_already_exists_for_unique_contract_bounds_error::IdentityPublicKeyAlreadyExistsForUniqueContractBoundsError, + }; use dash_sdk::dpp::consensus::state::voting::masternode_voted_too_many_times::MasternodeVotedTooManyTimesError; use dash_sdk::dpp::identity::Purpose; use dash_sdk::platform::Identifier; @@ -6547,6 +7544,681 @@ mod tests { ); } + fn broadcast_rejection(cause: ConsensusError) -> SdkError { + SdkError::StateTransitionBroadcastError(dash_sdk::error::StateTransitionBroadcastError { + code: 1, + message: "rejected".to_string(), + cause: Some(cause), + }) + } + + /// Every protocol-version-14 consensus rejection DET flows can hit maps to + /// its own variant, keeps the SDK error as its source, and reads as + /// complete sentences free of protocol jargon. + #[test] + fn protocol_v14_consensus_errors_map_to_their_variants() { + use dash_sdk::dpp::consensus::basic::contract_moderation::DocumentActionFeesWithoutModerationError; + use dash_sdk::dpp::consensus::basic::data_contract::PreProgrammedDistributionAmountOverLimitError; + use dash_sdk::dpp::consensus::basic::identity::{ + IdentityKeyLimitsUpdateEmptyError, IdentityPublicKeyLimitsNotAllowedError, + InvalidIdentityPublicKeyBudgetError, + }; + use dash_sdk::dpp::consensus::basic::token::InvalidTokenOncePerIdentityDistributionAmountError; + use dash_sdk::dpp::consensus::signature::PublicKeyWithLimitsCannotUpdateKeyLimitsError; + use dash_sdk::dpp::consensus::signature::{ + ContractBoundedKeyNonBatchError, ContractBoundedKeyOutOfBoundsError, + PublicKeyBudgetExhaustedError, PublicKeyExpiredError, + }; + use dash_sdk::dpp::consensus::state::contract_moderation::{ + ContractFeeClaimNotAllowedError, ContractFeesAlreadyClaimedThisEpochError, + ContractFeesNothingToClaimError, ContractUserBannedError, ContractUserSuspendedError, + }; + use dash_sdk::dpp::consensus::state::document::{ + document_action_fee_agreement_mismatch_error::DocumentActionFeeAgreementMismatchError, + document_action_fee_agreement_not_set_error::DocumentActionFeeAgreementNotSetError, + document_action_fee_multiplier_not_tolerated_error::DocumentActionFeeMultiplierNotToleratedError, + document_immutable_property_changed_error::DocumentImmutablePropertyChangedError, + referenced_document_type_not_deletable_error::ReferencedDocumentTypeNotDeletableError, + }; + use dash_sdk::dpp::consensus::state::identity::identity_public_key_already_expired_error::IdentityPublicKeyAlreadyExpiredError; + use dash_sdk::dpp::consensus::state::identity::identity_public_key_is_disabled_error::IdentityPublicKeyIsDisabledError; + use dash_sdk::dpp::consensus::state::identity::identity_public_key_limit_not_raised_error::IdentityPublicKeyLimitNotRaisedError; + use dash_sdk::dpp::consensus::state::identity::identity_public_key_limit_not_set_error::{ + IdentityPublicKeyLimitNotSetError, KeyLimit, + }; + use dash_sdk::dpp::consensus::state::identity::{ + gas_sponsor_insufficient_balance_error::GasSponsorInsufficientBalanceError, + identity_public_key_budget_exceeded_error::IdentityPublicKeyBudgetExceededError, + }; + use dash_sdk::dpp::consensus::state::token::{ + GasFeesPaidByNotAllowedError, InconsistentGasFeesPaidByInBatchError, + }; + use dash_sdk::dpp::data_contract::document_type::action_fees::ContractFeePot; + use dash_sdk::dpp::data_contract::document_type::action_fees::agreement::{ + AgreedFeeMultiplier, DocumentActionFeeAgreement, + }; + use dash_sdk::dpp::data_contract::document_type::action_fees::{ + ActionFeePricing, DocumentActionFee, + }; + use dash_sdk::dpp::identity::{Purpose, SecurityLevel}; + use dash_sdk::dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; + + let (contract_id, identity_id) = (Identifier::random(), Identifier::random()); + let fee = DocumentActionFee { + owner: 1_000, + moderators: 0, + }; + let agreed = AgreedFeeMultiplier { + known_permille: 1000, + increase_tolerance_percent: 20, + }; + let agreement = DocumentActionFeeAgreement::for_declared_fee( + ActionFeePricing::FeeMultiplier, + DocumentActionFee { + owner: 900, + moderators: 0, + }, + agreed, + ); + + type Expectation = fn(&TaskError) -> bool; + let cases: Vec<(ConsensusError, Expectation)> = vec![ + (PublicKeyExpiredError::new(3, 1_000, 2_000).into(), |e| { + matches!( + e, + TaskError::SigningKeyExpired { + key_id: 3, + expired_at_ms: 1_000, + .. + } + ) + }), + (PublicKeyBudgetExhaustedError::new(4).into(), |e| { + matches!(e, TaskError::SigningKeyBudgetExhausted { key_id: 4, .. }) + }), + ( + IdentityPublicKeyBudgetExceededError::new(identity_id, 5, 10, 20).into(), + |e| { + matches!( + e, + TaskError::SigningKeyBudgetExceeded { + key_id: 5, + remaining_budget: 10, + required_budget: 20, + .. + } + ) + }, + ), + (ContractBoundedKeyOutOfBoundsError::new(6).into(), |e| { + matches!( + e, + TaskError::SigningKeyOutOfContractBounds { key_id: 6, .. } + ) + }), + (ContractBoundedKeyNonBatchError::new(7).into(), |e| { + matches!( + e, + TaskError::SigningKeyContractBoundForNonBatch { key_id: 7, .. } + ) + }), + ( + DocumentActionFeeAgreementNotSetError::new( + "note".to_string(), + "create".to_string(), + ActionFeePricing::FeeMultiplier, + fee, + ) + .into(), + |e| matches!(e, TaskError::DocumentActionFeeNotAgreed { .. }), + ), + ( + DocumentActionFeeAgreementMismatchError::new( + "note".to_string(), + "create".to_string(), + ActionFeePricing::FeeMultiplier, + fee, + &agreement, + ) + .into(), + |e| matches!(e, TaskError::DocumentActionFeeChanged { .. }), + ), + ( + DocumentActionFeeMultiplierNotToleratedError::new( + "note".to_string(), + "create".to_string(), + agreed, + 1_500, + ) + .into(), + |e| { + matches!( + e, + TaskError::DocumentActionFeeMultiplierRose { + known_fee_multiplier_permille: 1000, + current_fee_multiplier_permille: 1_500, + increase_tolerance_percent: 20, + .. + } + ) + }, + ), + ( + ContractUserBannedError::new(contract_id, identity_id).into(), + |e| matches!(e, TaskError::ContractUserBanned { .. }), + ), + ( + ContractUserSuspendedError::new(contract_id, identity_id, 1_758_140_722_000).into(), + |e| { + matches!( + e, + TaskError::ContractUserSuspended { + until_ms: 1_758_140_722_000, + .. + } + ) + }, + ), + ( + GasSponsorInsufficientBalanceError::new(contract_id, 1, 2).into(), + |e| { + matches!( + e, + TaskError::GasSponsorInsufficientBalance { + balance: 1, + required_balance: 2, + .. + } + ) + }, + ), + ( + GasFeesPaidByNotAllowedError::new( + "note".to_string(), + "create".to_string(), + GasFeesPaidBy::ContractOwner, + GasFeesPaidBy::DocumentOwner, + ) + .into(), + |e| matches!(e, TaskError::GasFeesPaidByNotAllowed { .. }), + ), + ( + InconsistentGasFeesPaidByInBatchError::new(Some(identity_id), None).into(), + |e| matches!(e, TaskError::InconsistentGasFeesPayerInBatch { .. }), + ), + ( + DocumentImmutablePropertyChangedError::new( + Identifier::random(), + "note".to_string(), + "title".to_string(), + ) + .into(), + |e| matches!(e, TaskError::DocumentImmutablePropertyChanged { .. }), + ), + ( + ReferencedDocumentTypeNotDeletableError::new( + contract_id, + "note".to_string(), + "properties.ref".to_string(), + ) + .into(), + |e| matches!(e, TaskError::ReferencedDocumentTypeNotDeletable { .. }), + ), + ( + DocumentActionFeesWithoutModerationError::new("note".to_string()).into(), + |e| matches!(e, TaskError::DocumentActionFeesWithoutModeration { .. }), + ), + ( + InvalidTokenOncePerIdentityDistributionAmountError::new(0, 100).into(), + |e| { + matches!( + e, + TaskError::InvalidOncePerIdentityDistributionAmount { + amount: 0, + max_amount: 100, + .. + } + ) + }, + ), + ( + PreProgrammedDistributionAmountOverLimitError::new(1, 1_758_140_722_000).into(), + |e| { + matches!( + e, + TaskError::PreProgrammedDistributionOverLimit { + token_position: 1, + .. + } + ) + }, + ), + ]; + + let key_limit_cases: Vec<(ConsensusError, Expectation)> = vec![ + ( + IdentityPublicKeyAlreadyExpiredError::new(8, 1, 2).into(), + |e| matches!(e, TaskError::KeyExpiryInPast { key_id: 8, .. }), + ), + ( + IdentityPublicKeyLimitNotRaisedError::new(8, KeyLimit::Budget, 2, 1).into(), + |e| matches!(e, TaskError::KeyLimitNotRaised { key_id: 8, .. }), + ), + ( + IdentityPublicKeyLimitNotSetError::new(8, KeyLimit::Expiry).into(), + |e| matches!(e, TaskError::KeyLimitNotSet { key_id: 8, .. }), + ), + ( + PublicKeyWithLimitsCannotUpdateKeyLimitsError::new(9).into(), + |e| matches!(e, TaskError::KeyLimitsSignerHasLimits { key_id: 9, .. }), + ), + ( + IdentityPublicKeyLimitsNotAllowedError::new( + 8, + Purpose::TRANSFER, + SecurityLevel::CRITICAL, + ) + .into(), + |e| matches!(e, TaskError::KeyLimitsRefused { key_id: 8, .. }), + ), + (InvalidIdentityPublicKeyBudgetError::new(8).into(), |e| { + matches!(e, TaskError::KeyLimitsRefused { key_id: 8, .. }) + }), + (IdentityKeyLimitsUpdateEmptyError::new(8).into(), |e| { + matches!(e, TaskError::KeyLimitsRefused { key_id: 8, .. }) + }), + (IdentityPublicKeyIsDisabledError::new(8).into(), |e| { + matches!(e, TaskError::IdentityKeyDisabled { key_id: 8, .. }) + }), + ]; + + let contract_id = Identifier::random(); + let fee_pot_cases: Vec<(ConsensusError, Expectation)> = vec![ + ( + ContractFeeClaimNotAllowedError::new( + contract_id, + ContractFeePot::Owner, + Identifier::random(), + ) + .into(), + |e| matches!(e, TaskError::ContractFeeClaimNotAllowed { .. }), + ), + ( + ContractFeesAlreadyClaimedThisEpochError::new( + contract_id, + ContractFeePot::Owner, + 9, + ) + .into(), + |e| { + matches!( + e, + TaskError::ContractFeesAlreadyClaimedThisEpoch { epoch_index: 9, .. } + ) + }, + ), + ( + ContractFeesNothingToClaimError::new(contract_id, ContractFeePot::Moderators) + .into(), + |e| matches!(e, TaskError::ContractFeesNothingToClaim { .. }), + ), + ]; + + for (cause, expected) in cases + .into_iter() + .chain(key_limit_cases) + .chain(fee_pot_cases) + { + let debug = format!("{cause:?}"); + let error = TaskError::from(broadcast_rejection(cause)); + assert!(expected(&error), "{debug} mapped to {error:?}"); + assert!( + std::error::Error::source(&error).is_some(), + "{error:?} must keep the SDK error as its source" + ); + let message = error.to_string(); + assert!(message.ends_with('.'), "not a sentence: {message}"); + for jargon in ["consensus", "state transition", "nonce", "SDK", "permille"] { + assert!( + !message.contains(jargon), + "{message} contains jargon {jargon}" + ); + } + } + } + + /// The refusals protocol versions 14 and 15 add on username, vote and + /// token flows map to their own variants, keep the SDK error as their + /// source and tell the user what to do instead. + #[test] + fn contest_vote_and_token_transfer_refusals_map_to_their_variants() { + use dash_sdk::dpp::consensus::basic::data_contract::NonTransferableTokenTradeableError; + use dash_sdk::dpp::consensus::state::document::document_contest_maximum_contenders_reached_error::DocumentContestMaximumContendersReachedError; + use dash_sdk::dpp::consensus::state::document::referenced_document_preceded_error::ReferencedDocumentPrecededError; + use dash_sdk::dpp::consensus::state::token::TokenNotTransferableError; + use dash_sdk::dpp::consensus::state::voting::vote_choice_not_allowed_for_vote_poll_error::VoteChoiceNotAllowedForVotePollError; + use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use dash_sdk::dpp::voting::vote_polls::VotePoll; + use dash_sdk::dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + + type Expectation = fn(&TaskError) -> bool; + let token_id = Identifier::random(); + let poll = ContestedDocumentResourceVotePoll { + contract_id: Identifier::random(), + document_type_name: "domain".to_string(), + index_name: "parentNameAndLabel".to_string(), + index_values: vec![ + dash_sdk::dpp::platform_value::Value::from("dash"), + dash_sdk::dpp::platform_value::Value::from("alice"), + ], + }; + let cases: Vec<(ConsensusError, Expectation, &str)> = vec![ + ( + DocumentContestMaximumContendersReachedError::new(poll.clone(), 1000).into(), + |e| { + matches!( + e, + TaskError::DocumentContestContendersLimitReached { + max_contenders: 1000, + .. + } + ) + }, + "Choose a different username.", + ), + ( + TokenNotTransferableError::new(token_id, "transfer".to_string()).into(), + |e| matches!(e, TaskError::TokenNotTransferable { .. }), + "Use a different token.", + ), + ( + VoteChoiceNotAllowedForVotePollError::new( + VotePoll::ContestedDocumentResourceVotePoll(poll), + ResourceVoteChoice::Lock, + ) + .into(), + |e| matches!(e, TaskError::VoteChoiceNotAllowed { .. }), + "then vote again.", + ), + ( + ReferencedDocumentPrecededError::new( + Identifier::random(), + Identifier::random(), + "normalizedLabel".to_string(), + "preorder".to_string(), + ) + .into(), + |e| matches!(e, TaskError::ReferencedDocumentPreceded { .. }), + "Someone else asked for this username first.", + ), + ( + NonTransferableTokenTradeableError::new(3).into(), + |e| { + matches!( + e, + TaskError::NonTransferableTokenTradeable { + token_position: 3, + .. + } + ) + }, + "Allow transfers or turn trading off, then try again.", + ), + ]; + + for (cause, expected, action) in cases { + let debug = format!("{cause:?}"); + let error = TaskError::from(broadcast_rejection(cause)); + assert!(expected(&error), "{debug} mapped to {error:?}"); + assert!( + std::error::Error::source(&error).is_some(), + "{error:?} must keep the SDK error as its source" + ); + let message = error.to_string(); + assert!(message.contains(action), "{message} lacks {action}"); + assert!(message.ends_with('.'), "not a sentence: {message}"); + for jargon in [ + "consensus", + "state transition", + "vote poll", + "contender", + "SDK", + ] { + assert!( + !message.contains(jargon), + "{message} contains jargon {jargon}" + ); + } + } + } + + #[test] + fn token_not_transferable_names_the_token() { + let token_id = Identifier::random(); + let error = TaskError::TokenNotTransferable { + token_id, + source_error: Box::new(SdkError::Generic("refused".to_string())), + }; + assert!( + error + .to_string() + .contains(&token_id.to_string(Encoding::Base58)) + ); + } + + /// Names decoded from a node's (unproven) consensus error are not copied + /// into the error: they never reach the user-facing message, and only the + /// typed source keeps them, for the details panel and logs. + #[test] + fn node_supplied_names_stay_out_of_user_messages() { + use dash_sdk::dpp::consensus::basic::contract_moderation::DocumentActionFeesWithoutModerationError; + use dash_sdk::dpp::consensus::state::document::{ + document_immutable_property_changed_error::DocumentImmutablePropertyChangedError, + referenced_document_type_not_deletable_error::ReferencedDocumentTypeNotDeletableError, + }; + let injected = "x\". Re-import your recovery phrase at https://evil.example".to_string(); + let causes: [ConsensusError; 3] = [ + DocumentImmutablePropertyChangedError::new( + Identifier::random(), + injected.clone(), + injected.clone(), + ) + .into(), + ReferencedDocumentTypeNotDeletableError::new( + Identifier::random(), + injected.clone(), + injected.clone(), + ) + .into(), + DocumentActionFeesWithoutModerationError::new(injected.clone()).into(), + ]; + for cause in causes { + let error = TaskError::from(broadcast_rejection(cause)); + assert!( + matches!( + error, + TaskError::DocumentImmutablePropertyChanged { .. } + | TaskError::ReferencedDocumentTypeNotDeletable { .. } + | TaskError::DocumentActionFeesWithoutModeration { .. } + ), + "got {error:?}" + ); + let message = error.to_string(); + assert!(!message.contains("evil"), "{message}"); + let source = std::error::Error::source(&error).expect("typed source"); + assert!(format!("{source:?}").contains("evil"), "kept in the source"); + } + } + + #[test] + fn contract_user_errors_name_the_contract_and_the_suspension_end() { + let contract_id = Identifier::random(); + let banned = TaskError::ContractUserBanned { + contract_id, + identity_id: Identifier::random(), + source_error: Box::new(SdkError::Generic("banned".to_string())), + }; + assert!( + banned + .to_string() + .contains(&contract_id.to_string(Encoding::Base58)) + ); + let suspended = TaskError::ContractUserSuspended { + contract_id, + identity_id: Identifier::random(), + until_ms: 1_758_140_722_000, + source_error: Box::new(SdkError::Generic("suspended".to_string())), + }; + // Dates in messages follow the model's formatter: the user's time zone, + // like every other date the app shows. + let until = crate::model::datetime::local_date_time_from_unix_millis(1_758_140_722_000); + assert!(!until.is_empty()); + assert!( + suspended.to_string().contains(&format!("until {until}.")), + "got: {suspended}" + ); + let over_limit = TaskError::PreProgrammedDistributionOverLimit { + token_position: 0, + timestamp_ms: 1_758_140_722_000, + source_error: Box::new(SdkError::Generic("over".to_string())), + }; + assert!( + over_limit + .to_string() + .contains(&format!("at {until} add up")), + "got: {over_limit}" + ); + } + + #[test] + fn budget_exceeded_names_both_amounts_in_dash() { + let error = TaskError::SigningKeyBudgetExceeded { + key_id: 1, + remaining_budget: 50_000_000_000, + required_budget: 100_000_000_000, + source_error: Box::new(SdkError::Generic("budget".to_string())), + }; + let message = error.to_string(); + assert!(message.contains("costs 1 DASH"), "got: {message}"); + assert!(message.contains("only 0.5 DASH"), "got: {message}"); + } + + /// A second claim of a once-per-identity distribution maps to its own + /// variant carrying the ids, so the claim flow can remember the claim. + #[test] + fn once_per_identity_already_claimed_maps_to_its_variant() { + use dash_sdk::dpp::consensus::state::token::TokenOncePerIdentityDistributionAlreadyClaimedError; + let (token_id, identity_id) = (Identifier::random(), Identifier::random()); + let consensus = + ConsensusError::from(TokenOncePerIdentityDistributionAlreadyClaimedError::new( + token_id, + identity_id, + 1_758_140_722_000, + )); + let sdk_err = SdkError::StateTransitionBroadcastError( + dash_sdk::error::StateTransitionBroadcastError { + code: 40722, + message: "already claimed".to_string(), + cause: Some(consensus), + }, + ); + match TaskError::from(sdk_err) { + TaskError::TokenOncePerIdentityAlreadyClaimed { + token_id: t, + identity_id: i, + claimed_at_ms, + .. + } => { + assert_eq!( + (t, i, claimed_at_ms), + (token_id, identity_id, 1_758_140_722_000) + ); + } + other => panic!("got {other:?}"), + } + } + + /// An unclassified SDK failure behind a wallet-runtime token operation + /// keeps the operation, so the message names what did not complete. + #[test] + fn token_operation_failure_without_a_specific_cause_names_the_operation() { + let err = + TaskError::from_token_operation_failure("claim", SdkError::Generic("boom".to_string())); + assert!( + matches!( + err, + TaskError::TokenOperationFailed { + operation: TokenOperationKind::Claim, + .. + } + ), + "got {err:?}" + ); + assert_eq!( + err.to_string(), + "The token claim did not complete. Check your connection and try again." + ); + } + + /// A token operation rejected with a specific consensus cause keeps the + /// specific variant: the operation label must not mask a better message. + #[test] + fn token_operation_failure_with_a_specific_cause_keeps_it() { + use dash_sdk::dpp::consensus::state::token::IdentityTokenAccountNotFrozenError; + let consensus = ConsensusError::from(IdentityTokenAccountNotFrozenError::new( + Identifier::random(), + Identifier::random(), + "Unfreeze".to_string(), + )); + let sdk_err = SdkError::StateTransitionBroadcastError( + dash_sdk::error::StateTransitionBroadcastError { + code: 40703, + message: "identity token account is not frozen".to_string(), + cause: Some(consensus), + }, + ); + let err = TaskError::from_token_operation_failure("unfreeze", sdk_err); + assert!( + matches!(err, TaskError::TokenAccountNotFrozen { .. }), + "got {err:?}" + ); + } + + /// Every label upstream emits today maps to its own kind; an unknown one + /// degrades to the generic kind instead of failing. + #[test] + fn token_operation_labels_map_to_kinds() { + for (label, kind) in [ + ("claim", TokenOperationKind::Claim), + ("mint", TokenOperationKind::Mint), + ("burn", TokenOperationKind::Burn), + ("transfer", TokenOperationKind::Transfer), + ("freeze", TokenOperationKind::Freeze), + ("unfreeze", TokenOperationKind::Unfreeze), + ( + "destroy frozen funds", + TokenOperationKind::DestroyFrozenFunds, + ), + ("pause", TokenOperationKind::Pause), + ("resume", TokenOperationKind::Resume), + ("set price", TokenOperationKind::SetPrice), + ("purchase", TokenOperationKind::Purchase), + ("config update", TokenOperationKind::ConfigUpdate), + ( + "something new", + TokenOperationKind::Unrecognized { + label: "something new", + }, + ), + ] { + assert_eq!( + TokenOperationKind::from_upstream_label(label), + kind, + "{label}" + ); + } + } + #[test] fn test_identity_token_account_not_frozen_from_consensus_error() { use dash_sdk::dpp::consensus::state::token::IdentityTokenAccountNotFrozenError; diff --git a/src/backend_task/identity/add_key_to_identity.rs b/src/backend_task/identity/add_key_to_identity.rs index fb3954960..928fe47ee 100644 --- a/src/backend_task/identity/add_key_to_identity.rs +++ b/src/backend_task/identity/add_key_to_identity.rs @@ -2,9 +2,12 @@ use super::BackendTaskSuccessResult; use crate::backend_task::FeeResult; use crate::backend_task::error::TaskError; use crate::context::AppContext; +use crate::context::feature_gate::FeatureGate; use crate::model::derived_identity_key::{ derivation_index_limit, derivation_wallet, is_derivable_key_type, occupied_indices, }; +use crate::model::identity_key_limits::{contract_bounds_allowed, validate_key_limits}; +use crate::model::identity_key_usability::now_ms; use crate::model::qualified_identity::PrivateKeyTarget::{self, PrivateKeyOnMainIdentity}; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::qualified_identity::encrypted_key_storage::{ @@ -22,6 +25,7 @@ use dash_sdk::dpp::identity::hash::IdentityPublicKeyHashMethodsV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::{ IdentityPublicKeyGettersV0, IdentityPublicKeySettersV0, }; +use dash_sdk::dpp::identity::identity_public_key::accessors::v1::IdentityPublicKeyGettersV1; use dash_sdk::dpp::key_wallet::bip32::{DerivationPath, KeyDerivationType}; use dash_sdk::dpp::prelude::UserFeeIncrease; use dash_sdk::dpp::state_transition::identity_update_transition::IdentityUpdateTransition; @@ -31,6 +35,36 @@ use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::{Fetch, Identifier, Identity}; impl AppContext { + fn validate_added_key_configuration( + &self, + public_key_to_add: &QualifiedIdentityPublicKey, + ) -> Result<(), TaskError> { + // Key limits (protocol version 14) are checked before anything is + // prompted for or broadcast: an older network cannot decode such a + // key, and a limit Platform refuses would still be charged. + if public_key_to_add.identity_public_key.has_limits() { + if !FeatureGate::IdentityKeyLimits.is_available(self) { + return Err(TaskError::KeyLimitsNotSupported); + } + validate_key_limits(&public_key_to_add.identity_public_key, now_ms())?; + } + + { + let key = &public_key_to_add.identity_public_key; + if key.contract_bounds().is_some() + && !contract_bounds_allowed( + key.purpose(), + key.security_level(), + FeatureGate::ContractBoundAuthenticationKeys.is_available(self), + ) + { + return Err(TaskError::ContractBoundAuthenticationKeysNotSupported); + } + } + + Ok(()) + } + pub(super) async fn add_key_to_identity( &self, sdk: &Sdk, @@ -69,6 +103,7 @@ impl AppContext { index: u32, expected_key_id: KeyID, ) -> Result { + self.validate_added_key_configuration(&key)?; let (identity, key) = self .prepare_derived_identity_key(&identity.identity.id(), key, index) .await?; @@ -182,6 +217,8 @@ impl AppContext { // // A wallet-derived key has no private bytes to seal (it stays // protected by its wallet), so it needs no identity password (SEC-105). + self.validate_added_key_configuration(&public_key_to_add)?; + let verify_scope = match material { NewKeyMaterial::Private(_) => { self.protected_identity_verify_scope(&qualified_identity)? @@ -641,7 +678,7 @@ mod tests { async fn derived_key_persists_as_path_and_signs_after_reload() { use crate::context::test_staging::stage_identity_with_vaulted_keys; use crate::model::derived_identity_key::test_support::fixture; - use dash_sdk::dpp::dashcore::secp256k1::{Message, Secp256k1, SecretKey}; + use dash_sdk::dpp::secp256k1::{Message, SecretKey}; let staged = stage_identity_with_vaulted_keys([0xAA; 32], [0xBB; 32]).await; let (mut identity, cache, seed_hash, seed) = fixture(); @@ -703,14 +740,14 @@ mod tests { .get_resolve_with_seed(&placement, &wallets, &seed, Network::Testnet) .unwrap() .unwrap(); - let secp = Secp256k1::new(); - let secret = SecretKey::from_slice(bytes.as_ref()).unwrap(); + + let secret = SecretKey::from_secret_bytes(*bytes).unwrap(); let message = Message::from_digest([1; 32]); - let signature = secp.sign_ecdsa(&message, &secret); - secp.verify_ecdsa( - &message, + let signature = dash_sdk::dpp::secp256k1::ecdsa::sign(message, &secret); + dash_sdk::dpp::secp256k1::ecdsa::verify( &signature, - &cache.get(Network::Testnet, 0, 3).unwrap().inner, + message, + &cache.get(Network::Testnet, 0, 3).unwrap().inner.into(), ) .unwrap(); } @@ -833,14 +870,14 @@ mod tests { /// The stage's identity as published after another device added a key /// (not from this wallet) at key id 1. fn network_record_with_foreign_key(stage: &DerivedStage) -> Identity { - use dash_sdk::dpp::dashcore::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use dash_sdk::dpp::secp256k1::{PublicKey, SecretKey}; let mut record = stage.identity.identity.clone(); let mut foreign = record.public_keys()[&0].clone(); foreign.set_id(1); foreign.set_security_level(dash_sdk::dpp::identity::SecurityLevel::HIGH); - let secret = SecretKey::from_slice(&[9; 32]).unwrap(); + let secret = SecretKey::from_secret_bytes([9; 32]).unwrap(); foreign.set_data( - PublicKey::from_secret_key(&Secp256k1::new(), &secret) + PublicKey::from_secret_key(&secret) .serialize() .to_vec() .into(), @@ -1864,4 +1901,105 @@ mod tests { "nothing is written for an unloaded identity", ); } + + fn key_with_limits(total_budget: Option) -> QualifiedIdentityPublicKey { + use dash_sdk::dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; + use dash_sdk::dpp::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; + QualifiedIdentityPublicKey { + identity_public_key: IdentityPublicKey::from(IdentityPublicKeyV0 { + id: 5, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::ECDSA_SECP256K1, + read_only: false, + data: vec![2; 33].into(), + disabled_at: None, + }) + .with_limits(total_budget, Some(u64::MAX)), + in_wallet_at_derivation_path: None, + } + } + + #[tokio::test] + async fn a_key_with_limits_needs_a_network_that_supports_them() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + let sdk = dash_sdk::Sdk::new_mock(); + + // Not known yet (0), and mainnet's 13. + for version in [0, 13] { + ctx.set_platform_protocol_version(version); + let identity = super::super::key_limits_test_identity(); + let result = ctx + .add_key_to_identity(&sdk, identity, key_with_limits(Some(1)), [1; 32]) + .await; + + assert!( + matches!(result, Err(TaskError::KeyLimitsNotSupported)), + "protocol {version}: got {result:?}" + ); + } + } + + #[tokio::test] + async fn a_bound_authentication_key_needs_a_network_that_admits_it() { + use dash_sdk::dpp::identity::identity_public_key::contract_bounds::ContractBounds; + use dash_sdk::dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; + use dash_sdk::dpp::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + ctx.set_platform_protocol_version(13); + let sdk = dash_sdk::Sdk::new_mock(); + let key = QualifiedIdentityPublicKey { + identity_public_key: IdentityPublicKey::from(IdentityPublicKeyV0 { + id: 5, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: Some(ContractBounds::SingleContract { + id: dash_sdk::platform::Identifier::random(), + }), + key_type: KeyType::ECDSA_SECP256K1, + read_only: false, + data: vec![2; 33].into(), + disabled_at: None, + }), + in_wallet_at_derivation_path: None, + }; + + let result = ctx + .add_key_to_identity(&sdk, super::super::key_limits_test_identity(), key, [1; 32]) + .await; + + assert!( + matches!( + result, + Err(TaskError::ContractBoundAuthenticationKeysNotSupported) + ), + "got {result:?}" + ); + } + + #[tokio::test] + async fn a_key_with_a_zero_budget_is_refused_locally() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + ctx.set_platform_protocol_version(dash_sdk::dpp::version::v14::PROTOCOL_VERSION_14); + let sdk = dash_sdk::Sdk::new_mock(); + let identity = super::super::key_limits_test_identity(); + + let result = ctx + .add_key_to_identity(&sdk, identity, key_with_limits(Some(0)), [1; 32]) + .await; + + assert!( + matches!( + result, + Err(TaskError::InvalidKeyLimits { + source: crate::model::identity_key_limits::KeyLimitsError::ZeroBudget + }) + ), + "got {result:?}" + ); + } } diff --git a/src/backend_task/identity/dpns_usernames.rs b/src/backend_task/identity/dpns_usernames.rs index 3af5c4d0e..72473b787 100644 --- a/src/backend_task/identity/dpns_usernames.rs +++ b/src/backend_task/identity/dpns_usernames.rs @@ -1,4 +1,8 @@ //! Identity-side username ops: availability checks and own-request status. +//! +//! TODO(dpns-marketplace): selling, buying and transferring usernames through +//! the platform-wallet marketplace are not offered yet. See +//! https://github.com/dashpay/dash-evo-tool/issues/1103 use std::collections::BTreeMap; @@ -91,9 +95,10 @@ impl AppContext { /// The lookup for a registered name with this normalized label. pub(super) fn dpns_domain_query(&self, normalized: &str) -> DocumentQuery { DocumentQuery { + integer_range_clauses: vec![], sub_queries: Vec::new(), select: SelectProjection::documents(), - data_contract: self.dpns_contract.clone(), + data_contract: self.dpns_contract(), document_type_name: "domain".to_string(), where_clauses: vec![ WhereClause { @@ -127,7 +132,7 @@ impl AppContext { /// Convert a proved vote state into the pure snapshot the model decides on. fn contest_snapshot(&self, sdk: &Sdk, contenders: &Contenders) -> ContestSnapshot { let documents = self - .dpns_contract + .dpns_contract() .document_type_cloned_for_name("domain") .ok() .and_then(|document_type| { @@ -440,9 +445,10 @@ mod tests { async fn sdk_with_owned_name(ctx: &AppContext, id: Identifier) -> Sdk { let query = DocumentQuery { + integer_range_clauses: vec![], sub_queries: vec![], select: SelectProjection::documents(), - data_contract: ctx.dpns_contract.clone(), + data_contract: ctx.dpns_contract(), document_type_name: "domain".into(), where_clauses: vec![WhereClause { field: "records.identity".into(), @@ -458,6 +464,8 @@ mod tests { start: None, }; let document = Document::V0(dash_sdk::dpp::document::DocumentV0 { + moderated_at: None, + moderated_by: None, id: Identifier::from([88; 32]), owner_id: id, creator_id: None, @@ -604,7 +612,7 @@ mod tests { .expect_fetch_many::( ContestedDocumentVotePollDriveQuery { vote_poll: crate::model::dpns_voting::dpns_vote_poll( - &ctx.dpns_contract, + &ctx.dpns_contract(), &normalize_dpns_label("Alice"), ) .unwrap(), @@ -798,7 +806,7 @@ mod tests { .expect_fetch_many::( ContestedDocumentVotePollDriveQuery { vote_poll: crate::model::dpns_voting::dpns_vote_poll( - &ctx.dpns_contract, + &ctx.dpns_contract(), "d1ssh", ) .unwrap(), diff --git a/src/backend_task/identity/key_limits.rs b/src/backend_task/identity/key_limits.rs new file mode 100644 index 000000000..d4f6f2ac4 --- /dev/null +++ b/src/backend_task/identity/key_limits.rs @@ -0,0 +1,467 @@ +//! Usage limits of identity authentication keys (protocol version 14). + +use std::collections::BTreeMap; + +use std::sync::Arc; + +use crate::backend_task::error::TaskError; +use crate::context::AppContext; +use crate::context::feature_gate::FeatureGate; +use crate::model::identity_key_limits::{ + KeyLimitsError, KeyLimitsRaise, ReviewedSignerUnusable, reviewed_key_limits_signer, +}; +use crate::model::identity_key_usability::now_ms; +use crate::model::qualified_identity::QualifiedIdentity; +use dash_sdk::Sdk; +use dash_sdk::dpp::fee::Credits; +use dash_sdk::dpp::identity::KeyID; +use dash_sdk::dpp::identity::accessors::{IdentityGettersV0, IdentitySettersV0}; +use dash_sdk::dpp::identity::signer::Signer; +use dash_sdk::platform::Identity; +use dash_sdk::platform::identity_keys_remaining_budgets::{ + IdentityKeysRemainingBudgets, IdentityKeysRemainingBudgetsQuery, +}; +use dash_sdk::platform::transition::update_identity_key_limits::UpdateIdentityKeyLimits; +use dash_sdk::platform::{Fetch, Identifier}; +use dash_sdk::query_types::IdentityBalance; + +use super::BackendTaskSuccessResult; + +impl AppContext { + /// What is left of the budgets of `key_ids` of `identity_id`. + /// + /// One entry per requested key: `Some(credits)` for a budgeted key (zero + /// means the key can no longer sign), `None` for a key without a budget or + /// one Platform does not know. + pub(super) async fn fetch_key_remaining_budgets( + &self, + sdk: &Sdk, + identity_id: Identifier, + key_ids: Vec, + ) -> Result { + // A network below protocol version 14 answers this query on no node, + // so a request would exhaust the SDK's address pool. + if !FeatureGate::IdentityKeyLimits.is_available(self) { + return Err(TaskError::KeyRemainingBudgetsNotSupported); + } + if key_ids.is_empty() { + return Ok(BackendTaskSuccessResult::IdentityKeyRemainingBudgets { + identity_id, + budgets: BTreeMap::new(), + }); + } + let fetched = IdentityKeysRemainingBudgets::fetch( + sdk, + IdentityKeysRemainingBudgetsQuery { + identity_id, + key_ids: key_ids.clone(), + }, + ) + .await? + .unwrap_or_default(); + + Ok(BackendTaskSuccessResult::IdentityKeyRemainingBudgets { + identity_id, + budgets: remaining_budgets_by_key(&key_ids, &fetched), + }) + } +} + +impl AppContext { + /// Raise the limits of a key of `qualified_identity` exactly as reviewed + /// in `raise`. + /// + /// The key is read as Platform holds it now. If its limits are not the + /// ones the user reviewed (raised elsewhere meanwhile), nothing is signed: + /// the live keys replace the local copies and the raise is refused, so the + /// screen quotes it again. The same holds when the reviewed signer can no + /// longer sign it — another key is never picked in its place. The + /// updated key replaces the local copy. + pub(super) async fn raise_key_limits( + &self, + sdk: &Sdk, + qualified_identity: QualifiedIdentity, + raise: KeyLimitsRaise, + ) -> Result { + if !FeatureGate::IdentityKeyLimits.is_available(self) { + return Err(TaskError::KeyLimitsNotSupported); + } + let identity_id = qualified_identity.identity.id(); + let key_id = raise.key_id; + let identity = Identity::fetch_by_identifier(sdk, identity_id) + .await? + .ok_or(TaskError::IdentityMissingOnNetwork { identity_id })?; + let key = identity + .public_keys() + .get(&key_id) + .ok_or(TaskError::IdentityKeyNotFound { key_id })?; + let refusal = match raise.check_against(key, now_ms()) { + Err(error) => Some(TaskError::from(error)), + Ok(()) => { + reviewed_key_limits_signer(identity.public_keys(), raise.signing_key_id, |key| { + qualified_identity.can_sign_with(key) + }) + .err() + .map(refused_signer_error) + } + }; + if let Some(refusal) = refusal { + // Nothing is signed. Keep the keys as Platform holds them, so + // the screen's next review quotes the live limits and picks its + // signer from the live key set. + let live_keys: Vec<_> = identity.public_keys().values().cloned().collect(); + self.edit_local_qualified_identity(&identity_id, move |fresh| { + for key in live_keys { + fresh.identity.add_public_key(key); + } + Ok(()) + })?; + return Err(refusal); + } + + let signing_key = identity + .public_keys() + .get(&raise.signing_key_id) + .cloned() + .ok_or(TaskError::NoKeyLimitsSigningKey)?; + let signer = Arc::new(qualified_identity); + + let updated = identity + .update_key_limits( + sdk, + key_id, + raise.total_budget, + raise.expires_at, + Some(&signing_key), + Arc::clone(&signer), + None, + ) + .await?; + + // The update paid a fee, but its proof holds only the rewritten key: + // read the balance after it, proved, so the stored identity does not + // overstate what later fees can draw on. + let balance = IdentityBalance::fetch(sdk, identity_id) + .await + .map_err(TaskError::from); + self.finish_raise(identity_id, updated, balance) + } +} + +impl AppContext { + /// Store the outcome of a raise the network accepted: `key` as rewritten, + /// and `balance`, the identity's balance read after the update. + fn finish_raise( + &self, + identity_id: Identifier, + key: dash_sdk::dpp::identity::IdentityPublicKey, + balance: Result, TaskError>, + ) -> Result { + let (balance, balance_error) = match balance { + Ok(Some(balance)) => (Some(balance), None), + Ok(None) => (None, Some(None)), + Err(error) => (None, Some(Some(Box::new(error)))), + }; + self.persist_raised_key(&identity_id, key.clone(), balance)?; + // The raise is made and its key stored; only the balance is stale. + if let Some(source) = balance_error { + return Err(TaskError::KeyLimitsRaisedButBalanceNotRefreshed { source }); + } + Ok(BackendTaskSuccessResult::IdentityKeyLimitsRaised { identity_id, key }) + } + + /// Store `key`, whose limits the network has just raised, and `balance`, + /// when read, on the record of `identity_id`, in one write. The raise is + /// already on the network, so a failure here is reported as + /// [`TaskError::KeyLimitsRaisedButNotSaved`], not as a failed raise. + fn persist_raised_key( + &self, + identity_id: &Identifier, + key: dash_sdk::dpp::identity::IdentityPublicKey, + balance: Option, + ) -> Result<(), TaskError> { + self.edit_local_qualified_identity(identity_id, move |fresh| { + fresh.identity.add_public_key(key); + if let Some(balance) = balance { + fresh.identity.set_balance(balance); + } + Ok(()) + }) + .map(|_| ()) + .map_err(|source| TaskError::KeyLimitsRaisedButNotSaved { + source: Box::new(source), + }) + } +} + +/// The error for a reviewed signer Platform no longer accepts. +fn refused_signer_error(reason: ReviewedSignerUnusable) -> TaskError { + match reason { + ReviewedSignerUnusable::SignerChanged => KeyLimitsError::SignerChanged.into(), + ReviewedSignerUnusable::NoSigner => TaskError::NoKeyLimitsSigningKey, + } +} + +/// One entry per requested key id, in key id order: the remaining budget +/// Platform reported, `None` when it reported none. +fn remaining_budgets_by_key( + key_ids: &[KeyID], + fetched: &IdentityKeysRemainingBudgets, +) -> BTreeMap> { + key_ids + .iter() + .map(|key_id| (*key_id, fetched.get(key_id).copied().flatten())) + .collect() +} + +#[cfg(test)] +pub(super) mod tests { + use super::*; + use crate::backend_task::identity::IdentityTask; + use crate::backend_task::{BackendTask, BackendTaskContext}; + use crate::model::qualified_identity::{IdentityStatus, IdentityType}; + use dash_sdk::dpp::dashcore::Network; + use dash_sdk::dpp::version::PlatformVersion; + + pub(crate) fn user_identity() -> QualifiedIdentity { + let identity = + Identity::create_basic_identity(Identifier::random(), PlatformVersion::latest()) + .expect("basic identity"); + QualifiedIdentity { + identity, + associated_voter_identity: None, + associated_operator_identity: None, + associated_owner_key_id: None, + identity_type: IdentityType::User, + alias: None, + private_keys: Default::default(), + dpns_names: vec![], + associated_wallets: BTreeMap::new(), + secret_access: None, + wallet_index: None, + top_ups: Default::default(), + status: IdentityStatus::Active, + network: Network::Testnet, + } + } + + /// A reviewed signer replaced on Platform asks for a fresh review; only + /// when no key can sign at all is the user told to import one. + #[test] + fn a_changed_signer_asks_for_a_new_review_not_a_key_import() { + let changed = refused_signer_error(ReviewedSignerUnusable::SignerChanged); + assert!(matches!( + changed, + TaskError::InvalidKeyLimits { + source: KeyLimitsError::SignerChanged + } + )); + assert!(changed.to_string().contains("Review the change again")); + + assert!(matches!( + refused_signer_error(ReviewedSignerUnusable::NoSigner), + TaskError::NoKeyLimitsSigningKey + )); + } + + /// Protocol versions below 14: not known yet (0), and mainnet's 13. + const PRE_V14_PROTOCOL_VERSIONS: [u32; 2] = [0, 13]; + + /// A raise the network accepted stores the rewritten key and the balance + /// read after it, so the stored balance reflects the update's fee. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn a_raise_stores_the_key_and_the_balance_after_its_fee() { + use crate::context::test_staging::stage_identity_with_vaulted_keys; + use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dash_sdk::dpp::identity::identity_public_key::accessors::v1::IdentityPublicKeyGettersV1; + + let staged = stage_identity_with_vaulted_keys([0xA1; 32], [0xB1; 32]).await; + let ctx = &staged.ctx; + let stored = ctx + .get_local_qualified_identity(&staged.id) + .expect("read") + .expect("staged"); + let key = dash_sdk::dpp::identity::IdentityPublicKey::random_key( + 7, + Some(7), + PlatformVersion::latest(), + ) + .with_limits(Some(9_000), None); + let before = stored.identity.balance(); + + let result = ctx.finish_raise(staged.id, key.clone(), Ok(Some(before + 1_234))); + + assert!( + matches!( + result, + Ok(BackendTaskSuccessResult::IdentityKeyLimitsRaised { .. }) + ), + "got {result:?}" + ); + let after = ctx + .get_local_qualified_identity(&staged.id) + .expect("read") + .expect("staged"); + assert_eq!(after.identity.balance(), before + 1_234); + assert_eq!( + after + .identity + .public_keys() + .get(&key.id()) + .and_then(|k| k.total_budget()), + Some(9_000) + ); + } + + /// A balance that cannot be read after an accepted raise does not make the + /// raise a failure: the key is stored, and the user is told to refresh. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn an_unread_balance_after_a_raise_still_stores_the_key() { + use crate::context::test_staging::stage_identity_with_vaulted_keys; + use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dash_sdk::dpp::identity::identity_public_key::accessors::v1::IdentityPublicKeyGettersV1; + + let staged = stage_identity_with_vaulted_keys([0xA2; 32], [0xB2; 32]).await; + let ctx = &staged.ctx; + let stored = ctx + .get_local_qualified_identity(&staged.id) + .expect("read") + .expect("staged"); + let key = dash_sdk::dpp::identity::IdentityPublicKey::random_key( + 7, + Some(7), + PlatformVersion::latest(), + ) + .with_limits(Some(9_000), None); + + for balance in [Err(TaskError::IdentityNotFoundLocally), Ok(None)] { + let result = ctx.finish_raise(staged.id, key.clone(), balance); + assert!( + matches!( + result, + Err(TaskError::KeyLimitsRaisedButBalanceNotRefreshed { .. }) + ), + "got {result:?}" + ); + } + let after = ctx + .get_local_qualified_identity(&staged.id) + .expect("read") + .expect("staged"); + assert_eq!( + after + .identity + .public_keys() + .get(&key.id()) + .and_then(|k| k.total_budget()), + Some(9_000) + ); + assert_eq!(after.identity.balance(), stored.identity.balance()); + } + + /// A raise the network accepted is reported as made, not as failed, when + /// storing the raised key on this device fails afterward. + #[test] + fn a_raise_that_cannot_be_saved_is_reported_as_made() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + let identity = user_identity(); + let key = dash_sdk::dpp::identity::IdentityPublicKey::random_key( + 1, + Some(1), + PlatformVersion::latest(), + ); + + // Nothing is stored for this identity, so the write fails. + let result = ctx.persist_raised_key(&identity.identity.id(), key, Some(1)); + + assert!( + matches!(result, Err(TaskError::KeyLimitsRaisedButNotSaved { .. })), + "got {result:?}" + ); + } + + /// Before protocol version 14 is confirmed, no limits update is built or + /// broadcast: an older network cannot decode it. + #[tokio::test] + async fn raising_limits_is_refused_until_the_network_supports_them() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + let sdk = Sdk::new_mock(); + + for version in PRE_V14_PROTOCOL_VERSIONS { + ctx.set_platform_protocol_version(version); + let raise = KeyLimitsRaise { + key_id: 1, + seen_total_budget: Some(1), + seen_expires_at: None, + total_budget: Some(2), + expires_at: None, + signing_key_id: 0, + }; + let result = ctx.raise_key_limits(&sdk, user_identity(), raise).await; + + assert!( + matches!(result, Err(TaskError::KeyLimitsNotSupported)), + "protocol {version}: got {result:?}" + ); + } + } + + /// Before protocol version 14 is confirmed, the remaining-budget query is + /// not sent: an older network does not answer it, on any node. + #[tokio::test] + async fn remaining_budgets_are_not_queried_until_the_network_supports_them() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let ctx = crate::context::test_support::test_app_context(temp_dir.path()); + let sdk = Sdk::new_mock(); + + for version in PRE_V14_PROTOCOL_VERSIONS { + ctx.set_platform_protocol_version(version); + for key_ids in [vec![1, 2], vec![]] { + let result = ctx + .fetch_key_remaining_budgets(&sdk, Identifier::random(), key_ids) + .await; + assert!( + matches!(result, Err(TaskError::KeyRemainingBudgetsNotSupported)), + "protocol {version}: got {result:?}" + ); + } + } + } + + /// A failed raise must reach the screen it came from, keyed by identity + /// and key, so that screen re-enables its form. + #[test] + fn a_raise_is_attributed_to_its_identity_and_key() { + let identity = user_identity(); + let identity_id = identity.identity.id(); + let task = BackendTask::IdentityTask(IdentityTask::RaiseKeyLimits { + identity: Box::new(identity), + raise: KeyLimitsRaise { + key_id: 7, + seen_total_budget: Some(1), + seen_expires_at: None, + total_budget: Some(2), + expires_at: None, + signing_key_id: 0, + }, + }); + assert_eq!( + BackendTaskContext::from(&task).raise_key_limits_target(), + Some((identity_id, 7)) + ); + } + + #[test] + fn every_requested_key_gets_an_entry() { + let fetched: IdentityKeysRemainingBudgets = [(1, Some(500)), (2, None), (3, Some(0))] + .into_iter() + .collect(); + let by_key = remaining_budgets_by_key(&[1, 2, 3, 4], &fetched); + assert_eq!( + by_key, + BTreeMap::from([(1, Some(500)), (2, None), (3, Some(0)), (4, None)]) + ); + } +} diff --git a/src/backend_task/identity/load_identity.rs b/src/backend_task/identity/load_identity.rs index e6e687210..b7dc03e53 100644 --- a/src/backend_task/identity/load_identity.rs +++ b/src/backend_task/identity/load_identity.rs @@ -19,7 +19,7 @@ use crate::model::wallet::{Wallet, WalletSeedHash}; use crate::ui::identity::add_new_identity_screen::MAX_IDENTITY_INDEX; use dash_sdk::Sdk; use dash_sdk::dashcore_rpc::dashcore::PrivateKey; -use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; + use dash_sdk::dpp::dashcore::ProTxHash; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::identity::KeyType; @@ -268,10 +268,7 @@ impl AppContext { if let Ok(private_key) = PrivateKey::from_byte_array(&voting_private_key_bytes, self.network) { - let voting_address = private_key - .public_key(&Secp256k1::new()) - .pubkey_hash() - .to_byte_array(); + let voting_address = private_key.public_key().pubkey_hash().to_byte_array(); let voter_identifier = voter_identity_id( ProTxHash::from_byte_array(identity_id.to_buffer()), &voting_address, @@ -334,7 +331,6 @@ impl AppContext { }) .collect::, TaskError>>()?; - let secp = Secp256k1::new(); #[allow(clippy::type_complexity)] let (public_key_lookup, public_key_hash_lookup): ( HashMap, [u8; 32]>, @@ -342,12 +338,12 @@ impl AppContext { ) = input_private_keys .into_iter() .map(|private_key| { - let public_key = private_key.public_key(&secp); + let public_key = private_key.public_key(); let public_key_bytes = public_key.to_bytes(); let pub_key_hash = public_key.pubkey_hash().to_byte_array(); ( - (public_key_bytes, private_key.inner.secret_bytes()), - (pub_key_hash, private_key.inner.secret_bytes()), + (public_key_bytes, *private_key.inner.to_bytes()), + (pub_key_hash, *private_key.inner.to_bytes()), ) }) .unzip(); @@ -1042,7 +1038,7 @@ mod tests { .unwrap() .unwrap(); let private_key = PrivateKey::from_byte_array(&rand::random(), Network::Testnet).unwrap(); - let address = private_key.public_key(&Secp256k1::new()).pubkey_hash(); + let address = private_key.public_key().pubkey_hash(); let other_node = Identifier::from([0x75; 32]); let other_voter = Identifier::create_voter_identifier(other_node.as_bytes(), address.as_ref()); @@ -1125,7 +1121,7 @@ mod tests { .unwrap() .unwrap(); let private_key = PrivateKey::from_byte_array(&rand::random(), Network::Testnet).unwrap(); - let address = private_key.public_key(&Secp256k1::new()).pubkey_hash(); + let address = private_key.public_key().pubkey_hash(); let voter = Identifier::create_voter_identifier(staged.id.as_bytes(), address.as_ref()); let mut voter_identity = Identity::create_basic_identity(voter, PlatformVersion::latest()).unwrap(); diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index fb0a12944..4bdc82a69 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -1,6 +1,9 @@ mod add_key_to_identity; mod auth_pubkey_resolve; mod discover_identities; +mod key_limits; +#[cfg(test)] +pub(crate) use key_limits::tests::user_identity as key_limits_test_identity; mod dpns_usernames; mod load_identity; mod load_identity_by_dpns_name; @@ -548,6 +551,19 @@ pub enum IdentityTask { }, RefreshIdentity(QualifiedIdentity), RefreshLoadedIdentitiesOwnedDPNSNames, + /// Read what is left of the budgets of `key_ids` of `identity_id` + /// (protocol version 14 key limits). Read-only. + FetchKeyRemainingBudgets { + identity_id: Identifier, + key_ids: Vec, + }, + /// Raise the limits of one of the identity's keys (protocol version 14) + /// exactly as the user reviewed them: refused, not recomputed, when the + /// key's limits moved meanwhile. + RaiseKeyLimits { + identity: Box, + raise: crate::model::identity_key_limits::KeyLimitsRaise, + }, /// Check whether a username label can be requested right now. CheckUsernameAvailability { /// The identity that would request it. @@ -647,7 +663,7 @@ pub fn build_identity_registration_with_seed( identity_index: u32, funding_amount: Duffs, ) -> Result { - let dashpay_contract_id = app_context.dashpay_contract.id(); + let dashpay_contract_id = app_context.dashpay_contract().id(); let key_specs = default_identity_key_specs(dashpay_contract_id); let network = app_context.network; @@ -955,6 +971,16 @@ impl AppContext { IdentityTask::CheckLegacyRecovery { identity_id } => { self.check_legacy_recovery(identity_id) } + IdentityTask::FetchKeyRemainingBudgets { + identity_id, + key_ids, + } => { + self.fetch_key_remaining_budgets(sdk, identity_id, key_ids) + .await + } + IdentityTask::RaiseKeyLimits { identity, raise } => { + self.raise_key_limits(sdk, *identity, raise).await + } IdentityTask::RecoverLegacyIdentityData { identity_id, approved, @@ -1372,10 +1398,7 @@ mod tests { /// rejected or the wallet could not sign for its own identity. #[test] fn identity_key_entry_public_matches_private_derivation() { - use dash_sdk::dpp::dashcore::secp256k1::Secp256k1; - let seed = [0x42u8; 64]; - let secp = Secp256k1::new(); for network in [Network::Testnet, Network::Mainnet] { let wallet = Wallet::new_from_seed(seed, network, None, None).expect("build wallet"); @@ -1413,8 +1436,8 @@ mod tests { .to_priv(); assert_eq!( - entry.public_key.inner.serialize(), - private_key.public_key(&secp).inner.serialize(), + entry.public_key.inner.to_compressed(), + private_key.public_key().inner.to_compressed(), "public/private key drift at ({network:?}, id={identity_index}, key={key_index})" ); @@ -1501,12 +1524,12 @@ mod tests { .expect("resolve master") .expect("master present"); // The resolved private key's public key must match the spec's public key. - let secp = dash_sdk::dpp::dashcore::secp256k1::Secp256k1::new(); - let resolved_pub = dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&stored.1[..]) + + let resolved_pub = dash_sdk::dpp::secp256k1::SecretKey::from_secret_bytes(*stored.1) .expect("secret") - .public_key(&secp); + .public_key(); assert_eq!( - master.public_key.inner.serialize(), + master.public_key.inner.to_compressed(), resolved_pub.serialize(), "stored path resolves to the spec's public key" ); diff --git a/src/backend_task/identity/refresh_loaded_identities_dpns_names.rs b/src/backend_task/identity/refresh_loaded_identities_dpns_names.rs index 863e6b4c8..f0cdc4277 100644 --- a/src/backend_task/identity/refresh_loaded_identities_dpns_names.rs +++ b/src/backend_task/identity/refresh_loaded_identities_dpns_names.rs @@ -97,9 +97,10 @@ impl AppContext { /// The lookup for every DPNS name whose records point at `identity_id`. pub(super) fn owned_dpns_names_query(&self, identity_id: Identifier) -> DocumentQuery { DocumentQuery { + integer_range_clauses: vec![], sub_queries: Vec::new(), select: SelectProjection::documents(), - data_contract: self.dpns_contract.clone(), + data_contract: self.dpns_contract(), document_type_name: "domain".to_string(), where_clauses: vec![WhereClause { field: "records.identity".to_string(), @@ -177,6 +178,8 @@ mod tests { /// Mock the owned-names lookup of `owner` to return one name. async fn expect_owned_name(sdk: &mut dash_sdk::Sdk, ctx: &AppContext, owner: Identifier) { let document = Document::V0(dash_sdk::dpp::document::DocumentV0 { + moderated_at: None, + moderated_by: None, id: owner, owner_id: owner, creator_id: None, diff --git a/src/backend_task/identity/register_dpns_name.rs b/src/backend_task/identity/register_dpns_name.rs index 6fb7313a8..ca130a762 100644 --- a/src/backend_task/identity/register_dpns_name.rs +++ b/src/backend_task/identity/register_dpns_name.rs @@ -1,3 +1,4 @@ +use crate::model::identity_key_usability::SigningScope; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -30,7 +31,7 @@ use dash_sdk::{ use super::refresh_loaded_identities_dpns_names::name_acquired_at; use super::{BackendTaskSuccessResult, RegisterDpnsNameInput}; -use crate::model::dpns_usernames::{dpns_signing_requirement, key_can_sign_documents}; +use crate::model::dpns_usernames::{chosen_key_can_sign_documents, dpns_signing_requirement}; fn rebrand_dpns_domain_conflict(error: TaskError) -> TaskError { match error { @@ -98,6 +99,7 @@ async fn broadcast_name_registration( identity: &QualifiedIdentity, signing_key: IdentityPublicKey, label: &str, + approved_contest_fee: u64, register: F, ) -> Result where @@ -108,6 +110,7 @@ where let preorder_confirmed = Arc::new(AtomicBool::new(false)); let confirmed = Arc::clone(&preorder_confirmed); let input = SdkRegistrationInput { + contest_fund: Some(approved_contest_fee), label: label.to_owned(), identity: identity.identity.clone(), identity_public_key: signing_key, @@ -151,7 +154,7 @@ impl AppContext { return Err(TaskError::InvalidDpnsName { validation }); } - let dpns_contract = self.dpns_contract.clone(); + let dpns_contract = self.dpns_contract(); let qualified_identity = input.qualified_identity; @@ -188,10 +191,20 @@ impl AppContext { Some(key_id) => qualified_identity .identity .get_public_key_by_id(key_id) + // A key the user chose may have expired: the chooser offers + // it with a warning, and Platform decides by network time. .filter(|key| { - key_can_sign_documents(key, dpns_signing_requirement(&self.dpns_contract)) + chosen_key_can_sign_documents( + key, + dpns_signing_requirement(&self.dpns_contract()), + ) }), - None => qualified_identity.document_signing_key(&preorder_document_type), + None => qualified_identity.document_signing_key( + SigningScope::ContractWide { + contract_id: dpns_contract.id(), + }, + &preorder_document_type, + ), } .ok_or(TaskError::NoDocumentSigningKey)?; @@ -214,6 +227,7 @@ impl AppContext { &qualified_identity, public_key.clone(), &input.name_input, + input.approved_contest_fee, register, ) .await @@ -760,7 +774,7 @@ mod tests { let confirmed_preorder = domain_document(identity.identity.id(), "alice-123"); let failure = - broadcast_name_registration(&identity, key, "alice-123", |input| async move { + broadcast_name_registration(&identity, key, "alice-123", 0, |input| async move { // As the SDK does: report the confirmed first document if asked to. if let Some(report_confirmed) = input.preorder_callback { report_confirmed(&confirmed_preorder); @@ -776,6 +790,33 @@ mod tests { ); } + #[tokio::test] + async fn registration_never_auto_quotes_above_the_approved_contest_fund() { + for approved in [0, 50_000_000_000] { + let identity = bare_identity(4); + let key = dash_sdk::platform::IdentityPublicKey::random_key( + 1, + Some(4), + PlatformVersion::latest(), + ); + let _ = broadcast_name_registration( + &identity, + key, + "alice", + approved, + |input| async move { + assert_eq!( + input.contest_fund, + Some(approved), + "The SDK must receive the exact approved cap, including zero." + ); + Err(unproven_broadcast_failure()) + }, + ) + .await; + } + } + /// With no node to confirm the first document, the broadcast stops there. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn broadcast_that_cannot_confirm_the_first_document_reports_that_stage() { @@ -789,7 +830,7 @@ mod tests { PlatformVersion::latest(), ); - let failure = broadcast_name_registration(&identity, key, "alice-123", |input| { + let failure = broadcast_name_registration(&identity, key, "alice-123", 0, |input| { sdk.register_dpns_name(input) }) .await @@ -907,6 +948,8 @@ mod tests { /// A registered-name document as the network returns it for `label`. fn domain_document(owner: Identifier, label: &str) -> Document { Document::V0(DocumentV0 { + moderated_at: None, + moderated_by: None, id: Identifier::from([0x5D; 32]), owner_id: owner, creator_id: None, @@ -950,7 +993,7 @@ mod tests { .expect_fetch_many::( ContestedDocumentVotePollDriveQuery { vote_poll: crate::model::dpns_voting::dpns_vote_poll( - &ctx.dpns_contract, + &ctx.dpns_contract(), &normalize_dpns_label(label), ) .expect("vote poll"), diff --git a/src/backend_task/identity/top_up_identity.rs b/src/backend_task/identity/top_up_identity.rs index 8a6915f62..8303e2afb 100644 --- a/src/backend_task/identity/top_up_identity.rs +++ b/src/backend_task/identity/top_up_identity.rs @@ -259,7 +259,8 @@ impl AppContext { // Unreachable from here — the caller builds only the two modes // above — but this is a funds path, so an upstream funding mode // this path cannot build fails closed instead of panicking. - AssetLockFunding::DrainAccountBalance { .. } => { + AssetLockFunding::DrainAccountBalance { .. } + | AssetLockFunding::DrainWalletBalance { .. } => { return Err(TaskError::TopUpFundingMethodUnsupported); } }; diff --git a/src/backend_task/migration/finish_unwire.rs b/src/backend_task/migration/finish_unwire.rs index 8f48fbc81..721eda3de 100644 --- a/src/backend_task/migration/finish_unwire.rs +++ b/src/backend_task/migration/finish_unwire.rs @@ -518,7 +518,7 @@ async fn refresh_dapi_nodes_once_with_legacy_check( return; } - if let Err(error) = Arc::clone(app_context).reinit_core_client_and_sdk() { + if let Err(error) = app_context.rebuild_connection().await { tracing::warn!( target = "migration::finish_unwire", ?network, @@ -3955,7 +3955,7 @@ mod tests { use crate::wallet_backend::single_key::{ SingleKeyView, label_for_address, single_key_namespace_id, }; - use dash_sdk::dpp::dashcore::secp256k1::Secp256k1; + use dash_sdk::dpp::dashcore::{Address, Network, PrivateKey, PublicKey}; let dir = tempfile::tempdir().expect("tempdir"); @@ -3970,7 +3970,7 @@ mod tests { let priv_key = PrivateKey::from_byte_array(&raw, Network::Testnet).expect("priv"); let pub_key = PublicKey { compressed: priv_key.compressed, - inner: priv_key.inner.public_key(&Secp256k1::new()), + inner: priv_key.inner.public_key(), }; let address = Address::p2pkh(&pub_key, Network::Testnet).to_string(); let key_hash = crate::model::wallet::single_key::ClosedSingleKey::compute_key_hash(&raw); diff --git a/src/backend_task/migration/legacy_restore.rs b/src/backend_task/migration/legacy_restore.rs index 3dfe0e3b2..888c17b15 100644 --- a/src/backend_task/migration/legacy_restore.rs +++ b/src/backend_task/migration/legacy_restore.rs @@ -365,12 +365,12 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn review_regression_restore_counts_unreadable_protected_keys() { use crate::model::wallet::single_key::ClosedSingleKey; - use dash_sdk::dpp::dashcore::{Address, PrivateKey, PublicKey, secp256k1::Secp256k1}; + use dash_sdk::dpp::dashcore::{Address, PrivateKey, PublicKey}; let dir = tempfile::tempdir().expect("tempdir"); let ctx = app_context(dir.path()); let raw = [0x39; 32]; let private = PrivateKey::from_byte_array(&raw, ctx.network).unwrap(); - let public = PublicKey::new(private.inner.public_key(&Secp256k1::new())); + let public = PublicKey::new(private.inner.public_key()); let address = Address::p2pkh(&public, ctx.network).to_string(); let password = format!("test-password-{}", dir.path().display()); let envelope = ClosedSingleKey::encrypt_private_key(&raw, &password).unwrap(); diff --git a/src/backend_task/migration/single_key_restore.rs b/src/backend_task/migration/single_key_restore.rs index fd0db97cf..dacfc4065 100644 --- a/src/backend_task/migration/single_key_restore.rs +++ b/src/backend_task/migration/single_key_restore.rs @@ -29,7 +29,6 @@ use std::sync::Arc; -use dash_sdk::dpp::dashcore::secp256k1::Secp256k1; use dash_sdk::dpp::dashcore::{Address, Network, PrivateKey, PublicKey}; use rusqlite::{Connection, OpenFlags}; use zeroize::Zeroizing; @@ -333,10 +332,10 @@ pub(super) fn derive_p2pkh_address(wif: &str, network: Network) -> Result &'static TestSecrets { fn random_private_key() -> [u8; 32] { loop { let bytes = rand::random::<[u8; 32]>(); - if dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&bytes).is_ok() { + if dash_sdk::dpp::secp256k1::SecretKey::from_secret_bytes(bytes).is_ok() { return bytes; } } @@ -132,13 +132,12 @@ const V093_MASTERNODE_BLOB_HEX: &str = "00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb /// Populate only key payloads, preserving the captured v0.9.3 encoding layout. fn v093_masternode_blob() -> Vec { - use dash_sdk::dpp::dashcore::{ - hashes::{Hash, hash160}, - secp256k1::{PublicKey, Secp256k1, SecretKey}, - }; + use dash_sdk::dpp::dashcore::hashes::{Hash, hash160}; + use dash_sdk::dpp::secp256k1::PublicKey; let public_hash = |key: &[u8; 32]| { - let key = SecretKey::from_slice(key).expect("generated valid key"); - let public = PublicKey::from_secret_key(&Secp256k1::new(), &key); + let key = dash_sdk::dpp::secp256k1::SecretKey::from_secret_bytes(*(key)) + .expect("generated valid key"); + let public = PublicKey::from_secret_key(&key); hex::encode(hash160::Hash::hash(&public.serialize()).to_byte_array()) }; let encoded = V093_MASTERNODE_BLOB_HEX @@ -1642,12 +1641,11 @@ const SKIPPED_PROTECTED_ADDRESS: &str = "XprotectedPaperKeyAddress"; /// The P2PKH address of [`SKIPPED_SINGLE_KEY`] on mainnet. fn skipped_single_key_address() -> String { - use dash_sdk::dpp::dashcore::secp256k1::Secp256k1; use dash_sdk::dpp::dashcore::{Address, PrivateKey, PublicKey}; let key = PrivateKey::from_byte_array(&SKIPPED_SINGLE_KEY, Network::Mainnet).expect("key"); let public = PublicKey { compressed: true, - inner: key.inner.public_key(&Secp256k1::new()), + inner: key.inner.public_key(), }; Address::p2pkh(&public, Network::Mainnet).to_string() } diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index b0a3878e7..55e7ac283 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -1,36 +1,36 @@ use crate::app::TaskResult; -use crate::backend_task::error::{DapiAddressAvailability, TaskError}; use crate::backend_task::contested_names::ContestedResourceTask; use crate::backend_task::contract::ContractTask; use crate::backend_task::core::{CoreItem, CoreTask}; -use crate::backend_task::dashpay::{DashPayTask, ContactData}; +use crate::backend_task::dashpay::{ContactData, DashPayTask}; use crate::backend_task::document::DocumentTask; +use crate::backend_task::error::{DapiAddressAvailability, TaskError}; use crate::backend_task::identity::IdentityTask; use crate::backend_task::platform_info::{PlatformInfoTaskRequestType, PlatformInfoTaskResult}; use crate::backend_task::system_task::SystemTask; use crate::backend_task::wallet::WalletTask; use crate::context::AppContext; use crate::context::identity_load_registry::IdentityLoadToken; -use crate::model::masternode_input::decode_identity_id; use crate::model::dpns_voting::{ DpnsScheduledVoteClearOutcome, DpnsScheduledVoteKey, DpnsVoteOperationId, DpnsVoteTargetKey, }; -use dash_sdk::dpp::address_funds::PlatformAddress; -use dash_sdk::dpp::dashcore::Network; -use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::key_wallet::bip32::DerivationPath; +use crate::model::grovestark_prover::ProofDataOutput; +use crate::model::masternode_input::decode_identity_id; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::{PlatformAddressUpdates, WalletSeedHash}; -use crate::model::grovestark_prover::ProofDataOutput; use crate::ui::tokens::tokens_screen::{ ContractDescriptionInfo, IdentityTokenIdentifier, TokenInfo, }; use crate::utils::egui_mpsc::SenderAsync; use crate::utils::tasks::TaskManager; use contested_names::ScheduledDPNSVote; +use dash_sdk::dpp::address_funds::PlatformAddress; use dash_sdk::dpp::balances::credits::TokenAmount; -use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::evaluate_interval::IntervalEvaluationExplanation; +use dash_sdk::dpp::dashcore::Network; +use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function; use dash_sdk::dpp::group::group_action::GroupAction; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::key_wallet::bip32::DerivationPath; use dash_sdk::dpp::prelude::DataContract; use dash_sdk::dpp::state_transition::StateTransition; use dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule; @@ -38,19 +38,20 @@ use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0:: use dash_sdk::platform::{Document, DocumentQuery, Identifier}; use dash_sdk::query_types::{Documents, IndexMap}; use futures::future::join_all; +use grovestark::GroveSTARKTask; +use migration::MigrationTask; +use shielded::ShieldedTask; use std::collections::BTreeMap; use std::future::Future; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; -use migration::MigrationTask; -use shielded::ShieldedTask; use tokens::TokenTask; -use grovestark::GroveSTARKTask; pub mod broadcast_state_transition; pub mod contested_names; pub mod contract; +pub mod contract_fee_pots; pub mod core; pub mod dapi_discovery; pub mod dashpay; @@ -60,6 +61,8 @@ pub mod grovestark; pub mod identity; pub mod migration; pub mod platform_info; +#[cfg(test)] +mod protocol_13_transitions; pub mod register_contract; pub mod shielded; pub mod system_task; @@ -382,6 +385,13 @@ pub enum BackendTaskContext { LegacyRecoveryCheck(Identifier), /// The restore of one identity's approved legacy-recovery items. LegacyRecoveryRestore(Identifier), + /// The read of what is left of one identity's key budgets. + KeyRemainingBudgets(Identifier), + /// The raise of one identity key's limits. + RaiseKeyLimits { + identity_id: Identifier, + key_id: dash_sdk::dpp::identity::KeyID, + }, /// Warming the identity-auth public-key cache for one wallet identity /// index, so a screen can tell its own failed warm from other errors. IdentityAuthPubkeyWarm { @@ -582,6 +592,29 @@ impl BackendTaskContext { } } + /// The identity whose key budgets this operation read, or `None` for + /// anything else. + pub(crate) fn key_remaining_budgets_identity(&self) -> Option { + match self.operation() { + Self::KeyRemainingBudgets(identity_id) => Some(*identity_id), + _ => None, + } + } + + /// The identity key whose limits this operation raised, or `None` for + /// anything else. + pub(crate) fn raise_key_limits_target( + &self, + ) -> Option<(Identifier, dash_sdk::dpp::identity::KeyID)> { + match self.operation() { + Self::RaiseKeyLimits { + identity_id, + key_id, + } => Some((*identity_id, *key_id)), + _ => None, + } + } + /// The identity whose legacy-recovery offer this operation belongs to, or /// `None` for anything else. A screen showing the recovery affordance uses /// it to tell its own failed check or restore from any other task's error @@ -676,6 +709,16 @@ impl From<&BackendTask> for BackendTaskContext { identity_id, .. }) => Self::LegacyRecoveryRestore(*identity_id), + BackendTask::IdentityTask(IdentityTask::FetchKeyRemainingBudgets { + identity_id, + .. + }) => Self::KeyRemainingBudgets(*identity_id), + BackendTask::IdentityTask(IdentityTask::RaiseKeyLimits { identity, raise }) => { + Self::RaiseKeyLimits { + identity_id: identity.identity.id(), + key_id: raise.key_id, + } + } BackendTask::IdentityTask(IdentityTask::RefreshIdentity(identity)) => { Self::IdentityRefresh(identity.identity.id()) } @@ -810,7 +853,7 @@ pub enum BackendTaskSuccessResult { TokenEstimatedNonClaimedPerpetualDistributionAmountWithExplanation( IdentityTokenIdentifier, TokenAmount, - IntervalEvaluationExplanation, + distribution_function::evaluate_interval::IntervalEvaluationExplanation, ), ContractsWithDescriptions( BTreeMap, Vec)>, @@ -1026,6 +1069,8 @@ pub enum BackendTaskSuccessResult { SetTokenPrice(FeeResult), DestroyedFrozenFunds(FeeResult), ClaimedTokens(FeeResult), + /// The once-per-identity claim note of this pair was forgotten. + OncePerIdentityClaimHintCleared(IdentityTokenIdentifier), UpdatedTokenConfig(String, FeeResult), // The config item that was updated FetchedTokenBalances, /// A requested foreground refresh found the upstream balance sync already @@ -1083,6 +1128,39 @@ pub enum BackendTaskSuccessResult { /// The identity whose key protection was removed. identity_id: Identifier, }, + /// What is left of the budgets of an identity's keys, one entry per + /// requested key: `Some(credits)` for a budgeted key (zero means spent), + /// `None` for a key without a budget. + IdentityKeyRemainingBudgets { + identity_id: Identifier, + budgets: std::collections::BTreeMap< + dash_sdk::dpp::identity::KeyID, + Option, + >, + }, + /// Both fee pots of a contract, as Platform holds them. + ContractFeePots { + contract_id: Identifier, + pots: dash_sdk::platform::contract_fee_pots::ContractFeePots, + /// The seated moderation team, which claims an elected contract's + /// moderators pot instead of its interim team. + seated_moderators: crate::model::contract_fee_pots::SeatedModerators, + }, + /// A contract fee pot was paid out. `claimant_balance` is the claiming + /// identity's balance after the claim, `remaining_credits` what is left + /// in the pot. + ContractFeesClaimed { + contract_id: Identifier, + pot: dash_sdk::dpp::data_contract::document_type::action_fees::ContractFeePot, + claimant_balance: Option, + remaining_credits: dash_sdk::dpp::fee::Credits, + }, + /// The limits of an identity key were raised; `key` is the key as + /// Platform now stores it. + IdentityKeyLimitsRaised { + identity_id: Identifier, + key: dash_sdk::dpp::identity::IdentityPublicKey, + }, /// What the preserved legacy database could restore for this identity. /// Descriptors only — public key metadata, never key bytes. An empty plan /// means there is nothing to offer and the recovery affordance stays hidden. @@ -1430,7 +1508,7 @@ impl AppContext { Ok(self.run_migration_task(migration_task).await?) } BackendTask::ReinitCoreClientAndSdk => { - Arc::clone(self).reinit_core_client_and_sdk()?; + self.rebuild_connection().await?; Ok(BackendTaskSuccessResult::CoreClientReinitialized) } BackendTask::SwitchNetwork { network, start_spv } => { diff --git a/src/backend_task/platform_info.rs b/src/backend_task/platform_info.rs index fff1f014f..9be022f90 100644 --- a/src/backend_task/platform_info.rs +++ b/src/backend_task/platform_info.rs @@ -1,11 +1,13 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::backend_task::error::TaskError; -use crate::context::{AppContext, DET_PLATFORM_VERSION}; +use crate::context::AppContext; use crate::model::datetime; -use crate::model::fee_estimation::PlatformFeeEstimator; +use chrono::{LocalResult, prelude::*}; +use chrono_humanize::{Accuracy, HumanTime, Tense}; use dash_sdk::Error as SdkError; use dash_sdk::Sdk; use dash_sdk::dashcore_rpc::RpcApi; +use dash_sdk::dpp::address_funds::PlatformAddress; use dash_sdk::dpp::block::extended_epoch_info::{ ExtendedEpochInfo, v0::ExtendedEpochInfoV0Getters, }; @@ -13,22 +15,19 @@ use dash_sdk::dpp::core_types::validator_set::v0::ValidatorSetV0Getters; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::{Address, Network, ScriptBuf}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; -use dash_sdk::dpp::data_contracts::SystemDataContract; -use dash_sdk::dpp::data_contracts::withdrawals_contract::WithdrawalStatus; -use dash_sdk::dpp::data_contracts::withdrawals_contract::v1::document_types::withdrawal::properties::{ - AMOUNT, STATUS, TRANSACTION_INDEX, +use dash_sdk::dpp::data_contracts::withdrawals_contract::{ + WithdrawalStatus, + v1::document_types::withdrawal::properties::{AMOUNT, STATUS, TRANSACTION_INDEX}, }; use dash_sdk::dpp::document::{Document, DocumentV0Getters}; use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::platform_value::Value; use dash_sdk::dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; use dash_sdk::dpp::state_transition::identity_credit_withdrawal_transition::fields::OUTPUT_SCRIPT; -use dash_sdk::dpp::system_data_contracts::load_system_data_contract; use dash_sdk::dpp::version::PlatformVersion; use dash_sdk::dpp::withdrawal::daily_withdrawal_limit::daily_withdrawal_limit; use dash_sdk::dpp::{dash_to_credits, version::ProtocolVersionVoteCount}; -use dash_sdk::drive::query::{SelectProjection, OrderClause, WhereClause, WhereOperator}; -use dash_sdk::dpp::address_funds::PlatformAddress; +use dash_sdk::drive::query::{OrderClause, SelectProjection, WhereClause, WhereOperator}; use dash_sdk::platform::fetch_current_no_parameters::FetchCurrent; use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; use dash_sdk::platform::{ @@ -38,8 +37,6 @@ use dash_sdk::query_types::{ AddressInfo, CurrentQuorumsInfo, NoParamQuery, ProtocolVersionUpgrades, TotalCreditsInPlatform, }; use std::sync::Arc; -use chrono::{LocalResult, prelude::*}; -use chrono_humanize::{Accuracy, HumanTime, Tense}; #[derive(Debug, Clone, PartialEq)] pub enum PlatformInfoTaskRequestType { @@ -79,7 +76,9 @@ pub struct WithdrawalRecord { /// Amount in credits (atomic units). pub amount_credits: u64, /// Withdrawal status: `"queued"`, `"pooled"`, `"broadcasted"`, - /// `"complete"`, `"expired"`, or `"failed"`. + /// `"complete"`, `"expired"`, or `"failed"`. `"failed"` is terminal: the + /// payout was too small to ever be mined, and its credits are not returned + /// (they stay locked in the Core credit pool). pub status: String, /// Destination Dash address decoded from the output script, or `None` when /// the script does not map to a standard address on this network. @@ -252,15 +251,11 @@ fn format_extended_epoch_info( ) } -/// The degraded rendering used when the live epoch fetch fails. -/// +/// The epoch summary shown when the current epoch cannot be read. /// `protocol_version` is `None` while the connected network has not confirmed -/// one. The fee multiplier here is the app's own default, and the copy says so -/// rather than claiming anything about what networks charge: with the fetch -/// failed, this build cannot know that. A user comparing the figure against a -/// network that raised its fees has to be able to tell which of the two the app -/// is charging by. -fn format_hardcoded_current_epoch_info( +/// one; `fee_multiplier_permille` is the last known multiplier, not a fresh +/// network reading. +fn format_unavailable_current_epoch_info( protocol_version: Option, fee_multiplier_permille: u64, ) -> String { @@ -269,16 +264,16 @@ fn format_hardcoded_current_epoch_info( Some(protocol_version) => format!( "Current Epoch Information:\n\ • Protocol Version: {protocol_version}\n\ - • Fee Multiplier: {fee_multiplier}x (a fixed value, not read from the network)\n\n\ - Epoch details could not be read from the network just now. The multiplier shown \ - is this app's default, not a value read from the network. Try again in a moment." + • Fee Multiplier: {fee_multiplier}x (the last known value, not a fresh reading)\n\n\ + The current epoch could not be read from the network. Fee estimates use the last \ + known fee multiplier. Refresh again in a moment to read the current one." ), None => format!( "Current Epoch Information:\n\ • Protocol Version: the connected network has not confirmed one yet.\n\ - • Fee Multiplier: {fee_multiplier}x (a fixed value, not read from the network)\n\n\ - Epoch details could not be read from the network just now. The multiplier shown \ - is this app's default, not a value read from the network. Try again in a moment." + • Fee Multiplier: {fee_multiplier}x (the last known value, not a fresh reading)\n\n\ + The current epoch could not be read from the network. Fee estimates use the last \ + known fee multiplier. Refresh again in a moment to read the current one." ), } } @@ -321,51 +316,26 @@ fn format_current_quorums_info(current_quorums_info: &CurrentQuorumsInfo) -> Str result } -/// The daily withdrawal limit at [`DET_PLATFORM_VERSION`], never at the -/// newest version the upstream crates know. +/// The daily withdrawal limit the network enforces, at `platform_version` — +/// never at whatever version the upstream crates happen to know best. /// /// Protocol 13 uses `daily_withdrawal_limit` v1, a flat 2000 Dash that ignores /// the total. Protocol 14 (v2) derives it from the total credits Platform held a /// day ago, which the SDK cannot query; passing today's total is an accepted -/// gap for this display-only figure once DET moves to protocol 14. -fn local_daily_withdrawal_limit( +/// gap for this display-only figure. +fn network_daily_withdrawal_limit( total_credits_on_platform: Credits, + platform_version: &PlatformVersion, ) -> Result { - daily_withdrawal_limit(Some(total_credits_on_platform), DET_PLATFORM_VERSION) + daily_withdrawal_limit(Some(total_credits_on_platform), platform_version) .map_err(|e| WithdrawalParseError::DailyLimit(Box::new(e))) } -/// The Withdrawals system contract at [`DET_PLATFORM_VERSION`]. -/// The `status` values a finished withdrawal can carry, as the -/// `RecentlyCompletedWithdrawals` filter sends them. -/// -/// `FAILED` (5) exists only in the v2 withdrawals schema (protocol 14); the v1 -/// schema this build loads stops at `EXPIRED` (4). Sending it at protocol 13 is -/// still correct and deliberate: a JSON-schema `enum` constrains documents being -/// created, not query values. Nothing in the query path checks membership — the -/// `status` property is typed as a plain integer, and -/// `DocumentPropertyType::encode_value_for_tree_keys` encodes any value that -/// converts to it. So the out-of-enum member encodes, matches nothing on a -/// protocol-13 network (which cannot produce a failed withdrawal), and leaves -/// `COMPLETE` and `EXPIRED` matching as usual — while keeping the query complete -/// once the network, and DET's version pin, reach protocol 14. -fn completed_withdrawal_statuses() -> Vec { - vec![ - Value::U8(WithdrawalStatus::COMPLETE as u8), - Value::U8(WithdrawalStatus::EXPIRED as u8), - Value::U8(WithdrawalStatus::FAILED as u8), - ] -} - -fn withdrawals_contract() -> Result { - load_system_data_contract(SystemDataContract::Withdrawals, DET_PLATFORM_VERSION) - .map_err(|e| TaskError::from(SdkError::Protocol(e))) -} - fn format_withdrawal_documents_with_daily_limit( withdrawal_documents: &[Document], total_credits_on_platform: Credits, network: Network, + platform_version: &PlatformVersion, ) -> Result { let total_amount: Credits = withdrawal_documents .iter() @@ -384,7 +354,8 @@ fn format_withdrawal_documents_with_daily_limit( .map(|document| format_withdrawal_line(document, network)) .collect::, WithdrawalParseError>>()?; - let daily_withdrawal_limit = local_daily_withdrawal_limit(total_credits_on_platform)?; + let daily_withdrawal_limit = + network_daily_withdrawal_limit(total_credits_on_platform, platform_version)?; Ok(format!( "Withdrawal Information:\n\n\ @@ -470,6 +441,17 @@ fn format_withdrawal_line( )) } +/// Shown under a withdrawal history holding a failed withdrawal: that status +/// is terminal and, unlike an expired one, never gives the credits back. +const FAILED_WITHDRAWAL_NOTE: &str = "A failed withdrawal was too small to ever be paid out on the Dash network. Its amount is not returned to the identity. Withdraw larger amounts to avoid this."; + +/// Whether any of `documents` is a withdrawal in the terminal failed status. +fn any_withdrawal_failed(documents: &[Document]) -> bool { + documents.iter().any(|document| { + document.properties().get_integer::(STATUS).ok() == Some(WithdrawalStatus::FAILED as u8) + }) +} + /// Format one completed/expired/failed withdrawal document as a single line keyed by /// on-chain transaction index and last-update time: /// `"TX #: Dash for to
() at