diff --git a/.gitignore b/.gitignore index dd9496c8..4f0adac9 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,7 @@ dist-ssr *.sln *.sw? -# Internal docs +# Internal docs (root-level drafts) docs/ +# But keep SDK implementation docs +!src-tauri/crates/deadcat-sdk/docs/ diff --git a/justfile b/justfile index 327f30c8..9c87c47a 100644 --- a/justfile +++ b/justfile @@ -51,4 +51,4 @@ cargo-test: esac; \ ELEMENTSD_EXEC=$PWD/tests/elementsd-$TRIPLE \ ELECTRS_LIQUID_EXEC=$PWD/tests/electrs-$TRIPLE \ - cargo test + cargo test -- --test-threads=4 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4394e3b3..7b7be43d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -169,7 +169,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -179,9 +194,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -192,6 +207,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-query" version = "1.1.5" @@ -214,9 +238,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" @@ -296,7 +320,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -327,7 +351,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -338,7 +362,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -353,7 +377,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.3", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -373,7 +397,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -461,9 +485,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.3" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -471,9 +495,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.36.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -549,6 +573,21 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoin" version = "0.32.8" @@ -769,25 +808,26 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -813,9 +853,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byte-unit" @@ -950,9 +990,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -1025,9 +1065,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -1050,20 +1090,20 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "clap_lex", "strsim 0.11.1", @@ -1071,9 +1111,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" @@ -1086,9 +1126,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -1162,9 +1202,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.0", "core-foundation 0.10.1", @@ -1264,6 +1304,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -1271,7 +1324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1281,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1316,7 +1369,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1339,6 +1392,16 @@ dependencies = [ "darling_macro 0.21.3", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.13.4" @@ -1364,7 +1427,20 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.116", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.117", ] [[package]] @@ -1386,7 +1462,18 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.116", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", ] [[package]] @@ -1450,6 +1537,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "simplicity-sys", "simplicityhl", "tempfile", "thiserror 2.0.18", @@ -1472,9 +1560,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -1490,14 +1578,35 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.116", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", ] [[package]] name = "diesel" -version = "2.3.6" +version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d" +checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358" dependencies = [ "diesel_derives", "downcast-rs", @@ -1517,7 +1626,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1537,7 +1646,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" dependencies = [ - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1572,19 +1681,15 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.0", + "block2", + "libc", "objc2", ] @@ -1596,7 +1701,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1619,7 +1724,22 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", ] [[package]] @@ -1648,7 +1768,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1753,9 +1873,9 @@ dependencies = [ [[package]] name = "embed-resource" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f" dependencies = [ "cc", "memchr", @@ -1804,7 +1924,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1846,7 +1966,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "env_filter 1.0.0", "jiff", @@ -1861,9 +1981,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -2055,7 +2175,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2170,7 +2290,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2362,20 +2482,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -2462,7 +2582,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2553,7 +2673,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2691,10 +2811,20 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + [[package]] name = "http" version = "1.4.0" @@ -2872,7 +3002,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.116", + "syn 2.0.117", "unic-langid", ] @@ -2886,7 +3016,7 @@ dependencies = [ "i18n-config", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3118,9 +3248,9 @@ checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -3168,9 +3298,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "javascriptcore-rs" @@ -3197,9 +3327,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -3210,13 +3340,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3253,9 +3383,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -3312,10 +3442,10 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", + "cssparser 0.29.6", + "html5ever 0.29.1", "indexmap 2.13.0", - "selectors", + "selectors 0.24.0", ] [[package]] @@ -3356,9 +3486,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -3372,19 +3502,18 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.0", "libc", ] [[package]] name = "libsqlite3-sys" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ "pkg-config", "vcpkg", @@ -3420,9 +3549,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -3598,7 +3727,7 @@ source = "git+https://github.com/SatoshiPortal/boltz-rust?rev=d62288fdd1799d90df dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3610,9 +3739,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", ] [[package]] @@ -3623,7 +3763,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3948,9 +4088,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -3958,14 +4098,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3979,9 +4119,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -3995,38 +4135,8 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.11.0", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.11.0", - "objc2", "objc2-foundation", ] @@ -4054,41 +4164,6 @@ dependencies = [ "objc2-io-surface", ] -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -4112,7 +4187,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.0", "block2", - "libc", "objc2", "objc2-core-foundation", ] @@ -4128,16 +4202,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-javascript-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" -dependencies = [ - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -4150,17 +4214,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "objc2-security" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-ui-kit" version = "0.3.2" @@ -4185,15 +4238,13 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", - "objc2-javascript-core", - "objc2-security", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -4221,9 +4272,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.11.0", "cfg-if", @@ -4242,7 +4293,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4253,9 +4304,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -4396,7 +4447,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4439,6 +4490,17 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -4459,6 +4521,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -4489,6 +4561,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.10.0" @@ -4513,7 +4595,20 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -4543,31 +4638,40 @@ dependencies = [ "siphasher 1.0.2", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -4577,9 +4681,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -4628,7 +4732,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.5.2", "pin-project-lite", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -4663,9 +4767,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -4707,7 +4811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4732,11 +4836,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.5+spec-1.1.0", ] [[package]] @@ -4782,7 +4886,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4878,9 +4982,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -4913,9 +5017,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4926,6 +5030,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "r2d2" version = "0.8.10" @@ -5116,7 +5226,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5150,9 +5260,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" @@ -5386,7 +5496,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.116", + "syn 2.0.117", "walkdir", ] @@ -5452,22 +5562,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -5500,9 +5610,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -5551,9 +5661,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -5615,7 +5725,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5695,9 +5805,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", "core-foundation 0.10.1", @@ -5708,9 +5818,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -5723,14 +5833,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", - "derive_more", + "cssparser 0.29.6", + "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.0", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash 2.1.1", + "servo_arc 0.4.3", "smallvec", ] @@ -5818,7 +5947,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5829,7 +5958,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5854,7 +5983,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5899,9 +6028,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", @@ -5912,7 +6041,7 @@ dependencies = [ "schemars 1.2.1", "serde_core", "serde_json", - "serde_with_macros 3.16.1", + "serde_with_macros 3.18.0", "time", ] @@ -5930,14 +6059,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5959,7 +6088,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5972,6 +6101,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -6025,8 +6163,7 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "simplicity-lang" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e57bd4d84853974a212eab24ed89da54f49fbccf5e33e93bcd29f0a6591cd5" +source = "git+https://github.com/BlockstreamResearch/rust-simplicity.git?rev=8839c919d360b8eb4a3ade279c544448e9a2d0d3#8839c919d360b8eb4a3ade279c544448e9a2d0d3" dependencies = [ "bitcoin", "bitcoin_hashes", @@ -6042,9 +6179,8 @@ dependencies = [ [[package]] name = "simplicity-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bcb4e5bfc15080d67e0ce2c17d1c31bfb7521d65c86ea26ed0de72d5119d119" +version = "0.6.2" +source = "git+https://github.com/BlockstreamResearch/rust-simplicity.git?rev=8839c919d360b8eb4a3ade279c544448e9a2d0d3#8839c919d360b8eb4a3ade279c544448e9a2d0d3" dependencies = [ "bitcoin_hashes", "cc", @@ -6095,12 +6231,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6182,6 +6318,18 @@ dependencies = [ "serde", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + [[package]] name = "string_cache_codegen" version = "0.5.4" @@ -6194,6 +6342,18 @@ dependencies = [ "quote", ] +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.10.0" @@ -6236,9 +6396,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -6262,7 +6422,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6312,23 +6472,22 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.34.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" dependencies = [ "bitflags 2.11.0", "block2", "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", - "dispatch", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", @@ -6340,7 +6499,6 @@ dependencies = [ "once_cell", "parking_lot", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -6358,7 +6516,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6375,9 +6533,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.10.2" +version = "2.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" dependencies = [ "anyhow", "bytes", @@ -6426,9 +6584,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.5" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" dependencies = [ "anyhow", "cargo_toml", @@ -6448,9 +6606,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.5.4" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" dependencies = [ "base64 0.22.1", "brotli", @@ -6464,7 +6622,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.116", + "syn 2.0.117", "tauri-utils", "thiserror 2.0.18", "time", @@ -6475,23 +6633,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.4" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" dependencies = [ "anyhow", "glob", @@ -6550,9 +6708,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" dependencies = [ "cookie", "dpi", @@ -6575,9 +6733,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" dependencies = [ "gtk", "http", @@ -6585,7 +6743,6 @@ dependencies = [ "log", "objc2", "objc2-app-kit", - "objc2-foundation", "once_cell", "percent-encoding", "raw-window-handle", @@ -6602,9 +6759,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" dependencies = [ "anyhow", "brotli", @@ -6612,7 +6769,7 @@ dependencies = [ "ctor", "dunce", "glob", - "html5ever", + "html5ever 0.29.1", "http", "infer", "json-patch", @@ -6628,7 +6785,7 @@ dependencies = [ "serde", "serde-untagged", "serde_json", - "serde_with 3.16.1", + "serde_with 3.18.0", "swift-rs", "thiserror 2.0.18", "toml 0.9.12+spec-1.1.0", @@ -6651,14 +6808,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -6673,6 +6830,16 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -6725,7 +6892,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6736,7 +6903,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6785,9 +6952,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -6800,9 +6967,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -6815,13 +6982,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6934,7 +7101,7 @@ dependencies = [ "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] @@ -6955,6 +7122,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -6981,30 +7157,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" dependencies = [ "indexmap 2.13.0", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", - "winnow 0.7.14", + "winnow 1.0.0", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow 0.7.14", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tower" @@ -7070,7 +7246,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7178,13 +7354,13 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset 0.9.1", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -7353,11 +7529,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -7458,9 +7634,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -7471,9 +7647,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -7485,9 +7661,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7495,22 +7671,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -7564,9 +7740,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -7582,6 +7758,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -7672,7 +7860,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7811,7 +7999,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7822,7 +8010,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8174,9 +8362,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" dependencies = [ "memchr", ] @@ -8221,7 +8418,7 @@ dependencies = [ "heck 0.5.0", "indexmap 2.13.0", "prettyplease", - "syn 2.0.116", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -8237,7 +8434,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -8287,24 +8484,23 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.54.2" +version = "0.54.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" dependencies = [ "base64 0.22.1", "block2", "cookie", "crossbeam-channel", "dirs", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http", "javascriptcore-rs", "jni", - "kuchikiki", "libc", "ndk", "objc2", @@ -8391,15 +8587,15 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] [[package]] name = "zbus" -version = "5.13.2" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-executor", @@ -8417,14 +8613,14 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix 1.1.3", + "rustix 1.1.4", "serde", "serde_repr", "tracing", "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.14", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -8432,14 +8628,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.13.2" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -8452,28 +8648,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "winnow 0.7.14", + "winnow 0.7.15", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8493,7 +8689,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -8514,7 +8710,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8548,7 +8744,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8559,28 +8755,28 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" -version = "5.9.2" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", "serde", - "winnow 0.7.14", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.9.2" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "zvariant_utils", ] @@ -8593,6 +8789,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.116", - "winnow 0.7.14", + "syn 2.0.117", + "winnow 0.7.15", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 080f2b55..1cb10c0c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -63,3 +63,8 @@ rustls = { version = "0.23", default-features = false, features = ["std", "ring" [patch.crates-io] secp256k1-zkp = { git = "https://github.com/breez/rust-secp256k1-zkp.git", rev = "eac2e479255a6e32b5588bc25ee53c642fdd8395" } +# Pin to upstream master post-PR-#348 (right_shift_1 value bit corruption fix). +# Pre-jet-rename, so compatible with simplicityhl 0.4.1. +# Remove once simplicity-lang ≥0.7.1 is released with the fix. +simplicity-lang = { git = "https://github.com/BlockstreamResearch/rust-simplicity.git", rev = "8839c919d360b8eb4a3ade279c544448e9a2d0d3" } +simplicity-sys = { git = "https://github.com/BlockstreamResearch/rust-simplicity.git", rev = "8839c919d360b8eb4a3ade279c544448e9a2d0d3" } diff --git a/src-tauri/crates/deadcat-sdk/Cargo.toml b/src-tauri/crates/deadcat-sdk/Cargo.toml index fa6853b0..1962be9e 100644 --- a/src-tauri/crates/deadcat-sdk/Cargo.toml +++ b/src-tauri/crates/deadcat-sdk/Cargo.toml @@ -22,10 +22,15 @@ tokio = { version = "1", features = ["rt", "sync", "time"] } rayon = "1.11" [features] -testing = [] +testing = ["simplicity-sys/test-utils"] + +[dependencies.simplicity-sys] +version = "0.6" +optional = true [dev-dependencies] deadcat-sdk = { path = ".", features = ["testing"] } +simplicity-sys = { version = "0.6", features = ["test-utils"] } bitcoincore-rpc = "0.19" lwk_test_util = "0.14" nostr-relay-builder = "0.38" diff --git a/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-19-000001_add_order_creation_fields/down.sql b/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-19-000001_add_order_creation_fields/down.sql new file mode 100644 index 00000000..1da46d09 --- /dev/null +++ b/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-19-000001_add_order_creation_fields/down.sql @@ -0,0 +1 @@ +-- SQLite ALTER TABLE DROP COLUMN requires 3.35+; no-op for dev. diff --git a/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-19-000001_add_order_creation_fields/up.sql b/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-19-000001_add_order_creation_fields/up.sql new file mode 100644 index 00000000..a7010479 --- /dev/null +++ b/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-19-000001_add_order_creation_fields/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE maker_orders ADD COLUMN creation_txid TEXT; +ALTER TABLE maker_orders ADD COLUMN market_id TEXT; +ALTER TABLE maker_orders ADD COLUMN direction_label TEXT; +ALTER TABLE maker_orders ADD COLUMN offered_amount BIGINT; diff --git a/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-20-000001_create_price_history/down.sql b/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-20-000001_create_price_history/down.sql new file mode 100644 index 00000000..af152e55 --- /dev/null +++ b/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-20-000001_create_price_history/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS lmsr_price_history; diff --git a/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-20-000001_create_price_history/up.sql b/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-20-000001_create_price_history/up.sql new file mode 100644 index 00000000..90ce0983 --- /dev/null +++ b/src-tauri/crates/deadcat-sdk/deadcat-store/migrations/2026-03-20-000001_create_price_history/up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS lmsr_price_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pool_id TEXT NOT NULL, + market_id TEXT NOT NULL, + transition_txid TEXT NOT NULL, + old_s_index INTEGER NOT NULL, + new_s_index INTEGER NOT NULL, + reserve_yes INTEGER NOT NULL, + reserve_no INTEGER NOT NULL, + reserve_collateral INTEGER NOT NULL, + implied_yes_price_bps INTEGER NOT NULL, + recorded_at TEXT NOT NULL DEFAULT (datetime('now')), + block_height INTEGER +); +CREATE INDEX IF NOT EXISTS idx_price_history_market ON lmsr_price_history(market_id, recorded_at); +CREATE UNIQUE INDEX IF NOT EXISTS idx_price_history_txid ON lmsr_price_history(transition_txid); diff --git a/src-tauri/crates/deadcat-sdk/deadcat-store/src/conversions.rs b/src-tauri/crates/deadcat-sdk/deadcat-store/src/conversions.rs index 2d33e2d7..a719af6b 100644 --- a/src-tauri/crates/deadcat-sdk/deadcat-store/src/conversions.rs +++ b/src-tauri/crates/deadcat-sdk/deadcat-store/src/conversions.rs @@ -332,6 +332,10 @@ impl TryFrom<&MakerOrderRow> for MakerOrderInfo { nostr_event_json: row.nostr_event_json.clone(), created_at: row.created_at.clone(), updated_at: row.updated_at.clone(), + creation_txid: row.creation_txid.clone(), + market_id: row.market_id.clone(), + direction_label: row.direction_label.clone(), + offered_amount: row.offered_amount.map(|v| v as u64), }) } } @@ -373,6 +377,10 @@ pub fn new_maker_order_row( maker_receive_spk, nostr_event_id: nostr_event_id.map(|s| s.to_string()), nostr_event_json: nostr_event_json.map(|s| s.to_string()), + creation_txid: None, + market_id: None, + direction_label: None, + offered_amount: None, } } diff --git a/src-tauri/crates/deadcat-sdk/deadcat-store/src/lib.rs b/src-tauri/crates/deadcat-sdk/deadcat-store/src/lib.rs index 7ad81d55..e8090ec3 100644 --- a/src-tauri/crates/deadcat-sdk/deadcat-store/src/lib.rs +++ b/src-tauri/crates/deadcat-sdk/deadcat-store/src/lib.rs @@ -30,8 +30,9 @@ pub use deadcat_sdk::{ }; pub use error::StoreError; pub use store::{ - DeadcatStore, IssuanceData, MakerOrderInfo, MarketCandidateFilter, MarketCandidateInfo, - MarketFilter, MarketInfo, OrderFilter, OrderStatus, + DeadcatStore, IssuanceData, LmsrPoolFilter, LmsrPoolInfo, MakerOrderInfo, + MarketCandidateFilter, MarketCandidateInfo, MarketFilter, MarketInfo, OrderFilter, OrderStatus, + PriceHistoryEntry, PriceTransitionInput, }; pub use sync::{ChainSource, ChainUtxo, MarketStateChange, OrderStatusChange, SyncReport}; diff --git a/src-tauri/crates/deadcat-sdk/deadcat-store/src/models/maker_order.rs b/src-tauri/crates/deadcat-sdk/deadcat-store/src/models/maker_order.rs index 41c2a145..71f69f1d 100644 --- a/src-tauri/crates/deadcat-sdk/deadcat-store/src/models/maker_order.rs +++ b/src-tauri/crates/deadcat-sdk/deadcat-store/src/models/maker_order.rs @@ -26,6 +26,10 @@ pub struct MakerOrderRow { pub maker_receive_spk: Option>, pub nostr_event_id: Option, pub nostr_event_json: Option, + pub creation_txid: Option, + pub market_id: Option, + pub direction_label: Option, + pub offered_amount: Option, } #[derive(Debug, Clone, Insertable)] @@ -46,4 +50,8 @@ pub struct NewMakerOrderRow { pub maker_receive_spk: Option>, pub nostr_event_id: Option, pub nostr_event_json: Option, + pub creation_txid: Option, + pub market_id: Option, + pub direction_label: Option, + pub offered_amount: Option, } diff --git a/src-tauri/crates/deadcat-sdk/deadcat-store/src/schema.rs b/src-tauri/crates/deadcat-sdk/deadcat-store/src/schema.rs index d4d59675..5482226a 100644 --- a/src-tauri/crates/deadcat-sdk/deadcat-store/src/schema.rs +++ b/src-tauri/crates/deadcat-sdk/deadcat-store/src/schema.rs @@ -21,6 +21,10 @@ diesel::table! { maker_receive_spk -> Nullable, nostr_event_id -> Nullable, nostr_event_json -> Nullable, + creation_txid -> Nullable, + market_id -> Nullable, + direction_label -> Nullable, + offered_amount -> Nullable, } } diff --git a/src-tauri/crates/deadcat-sdk/deadcat-store/src/store.rs b/src-tauri/crates/deadcat-sdk/deadcat-store/src/store.rs index b8235db0..b4e19fab 100644 --- a/src-tauri/crates/deadcat-sdk/deadcat-store/src/store.rs +++ b/src-tauri/crates/deadcat-sdk/deadcat-store/src/store.rs @@ -145,6 +145,10 @@ pub struct MakerOrderInfo { pub nostr_event_json: Option, pub created_at: String, pub updated_at: String, + pub creation_txid: Option, + pub market_id: Option, + pub direction_label: Option, + pub offered_amount: Option, } #[derive(Debug, Clone, Default)] @@ -179,6 +183,178 @@ pub struct OrderFilter { pub limit: Option, } +// --- LMSR Pool types --- + +#[derive(Debug, Clone, Default)] +pub struct LmsrPoolFilter { + pub market_id: Option, + pub pool_id: Option, + pub limit: Option, +} + +#[derive(Debug, Clone)] +pub struct LmsrPoolInfo { + pub pool_id: String, + pub market_id: String, + pub creation_txid: String, + pub witness_schema_version: String, + pub current_s_index: u64, + pub reserve_yes: u64, + pub reserve_no: u64, + pub reserve_collateral: u64, + pub reserve_yes_outpoint: String, + pub reserve_no_outpoint: String, + pub reserve_collateral_outpoint: String, + pub state_source: String, + pub last_transition_txid: Option, + pub params_json: String, + pub nostr_event_id: Option, + pub nostr_event_json: Option, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, QueryableByName)] +struct LmsrPoolRow { + #[diesel(sql_type = diesel::sql_types::Text)] + pool_id: String, + #[diesel(sql_type = diesel::sql_types::Text)] + market_id: String, + #[diesel(sql_type = diesel::sql_types::Text)] + creation_txid: String, + #[diesel(sql_type = diesel::sql_types::Text)] + witness_schema_version: String, + #[diesel(sql_type = diesel::sql_types::BigInt)] + current_s_index: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + reserve_yes: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + reserve_no: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + reserve_collateral: i64, + #[diesel(sql_type = diesel::sql_types::Text)] + reserve_yes_outpoint: String, + #[diesel(sql_type = diesel::sql_types::Text)] + reserve_no_outpoint: String, + #[diesel(sql_type = diesel::sql_types::Text)] + reserve_collateral_outpoint: String, + #[diesel(sql_type = diesel::sql_types::Text)] + state_source: String, + #[diesel(sql_type = diesel::sql_types::Nullable)] + last_transition_txid: Option, + #[diesel(sql_type = diesel::sql_types::Text)] + params_json: String, + #[diesel(sql_type = diesel::sql_types::Nullable)] + nostr_event_id: Option, + #[diesel(sql_type = diesel::sql_types::Nullable)] + nostr_event_json: Option, + #[diesel(sql_type = diesel::sql_types::Text)] + created_at: String, + #[diesel(sql_type = diesel::sql_types::Text)] + updated_at: String, +} + +impl From for LmsrPoolInfo { + fn from(r: LmsrPoolRow) -> Self { + Self { + pool_id: r.pool_id, + market_id: r.market_id, + creation_txid: r.creation_txid, + witness_schema_version: r.witness_schema_version, + current_s_index: r.current_s_index as u64, + reserve_yes: r.reserve_yes as u64, + reserve_no: r.reserve_no as u64, + reserve_collateral: r.reserve_collateral as u64, + reserve_yes_outpoint: r.reserve_yes_outpoint, + reserve_no_outpoint: r.reserve_no_outpoint, + reserve_collateral_outpoint: r.reserve_collateral_outpoint, + state_source: r.state_source, + last_transition_txid: r.last_transition_txid, + params_json: r.params_json, + nostr_event_id: r.nostr_event_id, + nostr_event_json: r.nostr_event_json, + created_at: r.created_at, + updated_at: r.updated_at, + } + } +} + +#[derive(Debug)] +pub struct PriceTransitionInput { + pub pool_id: String, + pub market_id: String, + pub transition_txid: String, + pub old_s_index: u64, + pub new_s_index: u64, + pub reserve_yes: u64, + pub reserve_no: u64, + pub reserve_collateral: u64, + pub implied_yes_price_bps: u16, + pub block_height: Option, +} + +#[derive(Debug, Clone)] +pub struct PriceHistoryEntry { + pub pool_id: String, + pub market_id: String, + pub transition_txid: String, + pub old_s_index: u64, + pub new_s_index: u64, + pub reserve_yes: u64, + pub reserve_no: u64, + pub reserve_collateral: u64, + pub implied_yes_price_bps: u16, + pub recorded_at: String, + pub block_height: Option, +} + +#[derive(Debug, Clone, QueryableByName)] +#[allow(dead_code)] // `id` needed for SQL SELECT, unused in Rust +struct PriceHistoryRow { + #[diesel(sql_type = diesel::sql_types::Integer)] + id: i32, + #[diesel(sql_type = diesel::sql_types::Text)] + pool_id: String, + #[diesel(sql_type = diesel::sql_types::Text)] + market_id: String, + #[diesel(sql_type = diesel::sql_types::Text)] + transition_txid: String, + #[diesel(sql_type = diesel::sql_types::BigInt)] + old_s_index: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + new_s_index: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + reserve_yes: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + reserve_no: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + reserve_collateral: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + implied_yes_price_bps: i64, + #[diesel(sql_type = diesel::sql_types::Text)] + recorded_at: String, + #[diesel(sql_type = diesel::sql_types::Nullable)] + block_height: Option, +} + +impl From for PriceHistoryEntry { + fn from(r: PriceHistoryRow) -> Self { + Self { + pool_id: r.pool_id, + market_id: r.market_id, + transition_txid: r.transition_txid, + old_s_index: r.old_s_index as u64, + new_s_index: r.new_s_index as u64, + reserve_yes: r.reserve_yes as u64, + reserve_no: r.reserve_no as u64, + reserve_collateral: r.reserve_collateral as u64, + implied_yes_price_bps: r.implied_yes_price_bps as u16, + recorded_at: r.recorded_at, + block_height: r.block_height.map(|h| h as u32), + } + } +} + // --- DeadcatStore --- /// Persistent storage for deadcat prediction markets, maker orders, and UTXOs. @@ -571,6 +747,151 @@ impl DeadcatStore { Ok(()) } + // ==================== LMSR Pool Queries ==================== + + /// List LMSR pools, optionally filtered by market_id or pool_id. + pub fn list_lmsr_pools(&mut self, filter: &LmsrPoolFilter) -> crate::Result> { + use diesel::sql_types::{BigInt, Text}; + + let mut query = String::from( + "SELECT pool_id, market_id, creation_txid, witness_schema_version, + current_s_index, reserve_yes, reserve_no, reserve_collateral, + reserve_yes_outpoint, reserve_no_outpoint, reserve_collateral_outpoint, + state_source, last_transition_txid, params_json, + nostr_event_id, nostr_event_json, created_at, updated_at + FROM lmsr_pools WHERE 1=1", + ); + let mut bind_market: Option = None; + let mut bind_pool: Option = None; + let mut bind_limit: Option = None; + + if let Some(ref mid) = filter.market_id { + query.push_str(" AND market_id = ?"); + bind_market = Some(mid.clone()); + } + if let Some(ref pid) = filter.pool_id { + query.push_str(" AND pool_id = ?"); + bind_pool = Some(pid.clone()); + } + query.push_str(" ORDER BY updated_at DESC"); + if let Some(limit) = filter.limit { + query.push_str(" LIMIT ?"); + bind_limit = Some(limit); + } + + // Build the query dynamically based on bound parameters. + // Each combination of optional binds requires a separate arm because + // diesel's bound query types are not object-safe. + macro_rules! load_with_limit { + ($q:expr) => { + match bind_limit { + Some(l) => $q.bind::(l).load(&mut self.conn)?, + None => $q.load(&mut self.conn)?, + } + }; + } + + let rows: Vec = match (&bind_market, &bind_pool) { + (Some(m), Some(p)) => { + let q = diesel::sql_query(&query) + .bind::(m) + .bind::(p); + load_with_limit!(q) + } + (Some(m), None) => { + let q = diesel::sql_query(&query).bind::(m); + load_with_limit!(q) + } + (None, Some(p)) => { + let q = diesel::sql_query(&query).bind::(p); + load_with_limit!(q) + } + (None, None) => { + let q = diesel::sql_query(&query); + load_with_limit!(q) + } + }; + + Ok(rows.into_iter().map(LmsrPoolInfo::from).collect()) + } + + /// Record a price transition for market price history. + pub fn record_price_transition(&mut self, input: &PriceTransitionInput) -> crate::Result<()> { + use diesel::sql_types::{BigInt, Nullable, Text}; + + diesel::sql_query( + "INSERT OR IGNORE INTO lmsr_price_history + (pool_id, market_id, transition_txid, old_s_index, new_s_index, + reserve_yes, reserve_no, reserve_collateral, implied_yes_price_bps, + block_height) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind::(&input.pool_id) + .bind::(&input.market_id) + .bind::(&input.transition_txid) + .bind::(input.old_s_index as i64) + .bind::(input.new_s_index as i64) + .bind::(input.reserve_yes as i64) + .bind::(input.reserve_no as i64) + .bind::(input.reserve_collateral as i64) + .bind::(input.implied_yes_price_bps as i64) + .bind::, _>(input.block_height.map(|h| h as i64)) + .execute(&mut self.conn)?; + + Ok(()) + } + + /// Get market price history, optionally filtered by timestamp and limited. + pub fn get_market_price_history( + &mut self, + market_id: &str, + since: Option<&str>, + limit: Option, + ) -> crate::Result> { + use diesel::sql_types::{BigInt, Text}; + + let mut query = String::from( + "SELECT id, pool_id, market_id, transition_txid, old_s_index, new_s_index, + reserve_yes, reserve_no, reserve_collateral, implied_yes_price_bps, + recorded_at, block_height + FROM lmsr_price_history WHERE market_id = ?", + ); + let mut bind_since: Option = None; + let bind_limit: Option = limit; + if let Some(s) = since { + query.push_str(" AND recorded_at >= ?"); + bind_since = Some(s.to_string()); + } + query.push_str(" ORDER BY recorded_at ASC"); + if bind_limit.is_some() { + query.push_str(" LIMIT ?"); + } + + macro_rules! load_with_limit { + ($q:expr) => { + match bind_limit { + Some(l) => $q.bind::(l).load(&mut self.conn)?, + None => $q.load(&mut self.conn)?, + } + }; + } + + let rows: Vec = match &bind_since { + Some(s) => { + let q = diesel::sql_query(&query) + .bind::(market_id) + .bind::(s); + load_with_limit!(q) + } + None => { + let q = diesel::sql_query(&query).bind::(market_id); + load_with_limit!(q) + } + }; + + Ok(rows.into_iter().map(PriceHistoryEntry::from).collect()) + } + // ==================== Market Queries ==================== fn load_candidate(&mut self, candidate_id: i32) -> crate::Result { @@ -977,6 +1298,36 @@ impl DeadcatStore { Ok(()) } + /// Record local creation metadata for an order that was just created on-chain. + /// Matches by `(cmr, maker_base_pubkey)` — the same unique key used by `ingest_maker_order`. + pub fn record_order_creation( + &mut self, + cmr: &[u8], + maker_base_pubkey: &[u8; 32], + creation_txid: &str, + market_id: &str, + direction_label: &str, + offered_amount: u64, + ) -> crate::Result<()> { + diesel::update( + maker_orders::table.filter( + maker_orders::cmr + .eq(cmr) + .and(maker_orders::maker_base_pubkey.eq(maker_base_pubkey.to_vec())), + ), + ) + .set(( + maker_orders::creation_txid.eq(creation_txid), + maker_orders::market_id.eq(market_id), + maker_orders::direction_label.eq(direction_label), + maker_orders::offered_amount.eq(offered_amount as i64), + maker_orders::updated_at.eq(diesel::dsl::sql::(DATETIME_NOW)), + )) + .execute(&mut self.conn)?; + + Ok(()) + } + // ==================== Issuance Data ==================== /// Manually set issuance entropy for a market (fallback if not yet synced from chain). @@ -2045,4 +2396,177 @@ mod tests { ); assert_eq!(row.nostr_event_id.as_deref(), Some("evt-3")); } + + // ── list_lmsr_pools tests ──────────────────────────────────────────── + + #[test] + fn list_lmsr_pools_returns_all() { + let mut store = DeadcatStore::open_in_memory().unwrap(); + let mut p1 = sample_lmsr_pool_ingest(); + p1.pool_id = "aa".repeat(32); + let mut p2 = sample_lmsr_pool_ingest(); + p2.pool_id = "bb".repeat(32); + store.ingest_lmsr_pool(&p1).unwrap(); + store.ingest_lmsr_pool(&p2).unwrap(); + + let pools = store.list_lmsr_pools(&LmsrPoolFilter::default()).unwrap(); + assert_eq!(pools.len(), 2); + } + + #[test] + fn list_lmsr_pools_filters_by_market_id() { + let mut store = DeadcatStore::open_in_memory().unwrap(); + let mut p1 = sample_lmsr_pool_ingest(); + p1.pool_id = "aa".repeat(32); + p1.market_id = "11".repeat(32); + let mut p2 = sample_lmsr_pool_ingest(); + p2.pool_id = "bb".repeat(32); + p2.market_id = "22".repeat(32); + store.ingest_lmsr_pool(&p1).unwrap(); + store.ingest_lmsr_pool(&p2).unwrap(); + + let pools = store + .list_lmsr_pools(&LmsrPoolFilter { + market_id: Some("11".repeat(32)), + ..Default::default() + }) + .unwrap(); + assert_eq!(pools.len(), 1); + assert_eq!(pools[0].market_id, "11".repeat(32)); + } + + #[test] + fn list_lmsr_pools_filters_by_pool_id() { + let mut store = DeadcatStore::open_in_memory().unwrap(); + let mut p1 = sample_lmsr_pool_ingest(); + p1.pool_id = "aa".repeat(32); + let mut p2 = sample_lmsr_pool_ingest(); + p2.pool_id = "bb".repeat(32); + store.ingest_lmsr_pool(&p1).unwrap(); + store.ingest_lmsr_pool(&p2).unwrap(); + + let pools = store + .list_lmsr_pools(&LmsrPoolFilter { + pool_id: Some("bb".repeat(32)), + ..Default::default() + }) + .unwrap(); + assert_eq!(pools.len(), 1); + assert_eq!(pools[0].pool_id, "bb".repeat(32)); + } + + // ── price history tests ────────────────────────────────────────────── + + fn sample_price_transition(pool_id: &str, market_id: &str, txid: &str) -> PriceTransitionInput { + PriceTransitionInput { + pool_id: pool_id.to_string(), + market_id: market_id.to_string(), + transition_txid: txid.to_string(), + old_s_index: 4, + new_s_index: 5, + reserve_yes: 500, + reserve_no: 400, + reserve_collateral: 1000, + implied_yes_price_bps: 5500, + block_height: Some(100), + } + } + + #[test] + fn record_price_transition_and_query() { + let mut store = DeadcatStore::open_in_memory().unwrap(); + let input = sample_price_transition("pool-1", "market-1", "tx-1"); + store.record_price_transition(&input).unwrap(); + + let entries = store + .get_market_price_history("market-1", None, None) + .unwrap(); + assert_eq!(entries.len(), 1); + let e = &entries[0]; + assert_eq!(e.pool_id, "pool-1"); + assert_eq!(e.market_id, "market-1"); + assert_eq!(e.transition_txid, "tx-1"); + assert_eq!(e.old_s_index, 4); + assert_eq!(e.new_s_index, 5); + assert_eq!(e.reserve_yes, 500); + assert_eq!(e.reserve_no, 400); + assert_eq!(e.reserve_collateral, 1000); + assert_eq!(e.implied_yes_price_bps, 5500); + assert_eq!(e.block_height, Some(100)); + } + + #[test] + fn record_price_transition_deduplicates_by_txid() { + let mut store = DeadcatStore::open_in_memory().unwrap(); + let input = sample_price_transition("pool-1", "market-1", "tx-dup"); + store.record_price_transition(&input).unwrap(); + store.record_price_transition(&input).unwrap(); // second insert is no-op + + let entries = store + .get_market_price_history("market-1", None, None) + .unwrap(); + assert_eq!(entries.len(), 1); + } + + #[test] + fn get_market_price_history_filters_by_market() { + let mut store = DeadcatStore::open_in_memory().unwrap(); + store + .record_price_transition(&sample_price_transition("pool-1", "market-A", "tx-1")) + .unwrap(); + store + .record_price_transition(&sample_price_transition("pool-1", "market-B", "tx-2")) + .unwrap(); + + let entries = store + .get_market_price_history("market-A", None, None) + .unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].market_id, "market-A"); + } + + #[test] + fn get_market_price_history_orders_by_recorded_at() { + let mut store = DeadcatStore::open_in_memory().unwrap(); + // Insert out of order — recorded_at is auto-set by SQLite's datetime('now') + // so they'll all have the same timestamp. Use distinct txids to verify ordering + // remains deterministic (ASC by recorded_at, then by rowid). + for i in 0..3 { + store + .record_price_transition(&sample_price_transition( + "pool-1", + "market-1", + &format!("tx-{i}"), + )) + .unwrap(); + } + + let entries = store + .get_market_price_history("market-1", None, None) + .unwrap(); + assert_eq!(entries.len(), 3); + // Verify ascending order by checking txids come back in insertion order + assert_eq!(entries[0].transition_txid, "tx-0"); + assert_eq!(entries[1].transition_txid, "tx-1"); + assert_eq!(entries[2].transition_txid, "tx-2"); + } + + #[test] + fn get_market_price_history_respects_limit() { + let mut store = DeadcatStore::open_in_memory().unwrap(); + for i in 0..5 { + store + .record_price_transition(&sample_price_transition( + "pool-1", + "market-1", + &format!("tx-{i}"), + )) + .unwrap(); + } + + let entries = store + .get_market_price_history("market-1", None, Some(2)) + .unwrap(); + assert_eq!(entries.len(), 2); + } } diff --git a/src-tauri/crates/deadcat-sdk/docs/lmsr-admin-adjust-testing.md b/src-tauri/crates/deadcat-sdk/docs/lmsr-admin-adjust-testing.md new file mode 100644 index 00000000..1e939e04 --- /dev/null +++ b/src-tauri/crates/deadcat-sdk/docs/lmsr-admin-adjust-testing.md @@ -0,0 +1,219 @@ +# LMSR AdminAdjust — Testing Design & Lessons Learned + +**Date:** 2026-03-20 + +--- + +## What this covers + +The `adjust_lmsr_pool` SDK method lets a pool admin add or remove liquidity +from an LMSR reserve bundle on Liquid. It constructs a PSET with covenant +inputs (explicit reserve UTXOs) and optional wallet inputs (confidential), +computes a BIP-340 admin signature over a Simplicity-specific message hash, +attaches Simplicity witnesses, and broadcasts. + +Getting this to work end-to-end required fixing four layered bugs, each +invisible until the previous one was resolved. This document captures the +testing architecture and the reasoning behind key design decisions so future +work doesn't have to re-derive them. + +--- + +## Test matrix + +| Test | Type | What it verifies | Runtime | +|---|---|---|---| +| `admin_adjust_c_evaluator_agrees_with_rust` | Unit | Rust BitMachine + C evaluator both accept the admin adjust program with a mock env | ~2s | +| `adjust_lmsr_pool_increases_reserves_regtest` | Integration | Full increase-liquidity flow on regtest (wallet inputs, blinding, broadcast, rescan) | ~20s | +| `adjust_lmsr_pool_decreases_reserves_regtest` | Integration | Full decrease-liquidity flow on regtest (no wallet inputs, fee from surplus, broadcast, rescan) | ~20s | +| All 6 `lmsr_pool` integration tests | Integration | Create, scan, trade, adjust (increase + decrease), validation rejection | ~105s | + +--- + +## Bug 1: Zero genesis hash in assembly env + +**Symptom:** Admin signature passed locally but `elementsd` rejected with +"Assertion failed inside jet". + +**Root cause:** `attach_lmsr_pool_witnesses` used +`BlockHash::all_zeros()` for the `ElementsEnv` genesis hash. The swap path +never exercises `jet::genesis_block_hash()` directly (only via the +self-consistent `sig_all_hash`), so swaps worked. The admin path explicitly +hashes `genesis_block_hash()` into the admin signature message — a zero hash +produced a different message than on-chain. + +**Fix:** Added `genesis_hash: [u8; 32]` parameter to +`attach_lmsr_pool_witnesses`. Callers pass the real genesis hash. + +**Testing insight:** The unit test `admin_adjust_c_evaluator_agrees_with_rust` +uses `genesis_hash = [0u8; 32]` — this is fine because both the signature +computation and the env use the same zeros, so they're self-consistent. The +integration tests use the real chain genesis, which is what matters for +on-chain acceptance. + +--- + +## Bug 2: Hardcoded regtest genesis hash + +**Symptom:** After fixing bug 1, admin adjust still failed on regtest with +"Assertion failed inside jet". + +**Root cause:** `Network::LiquidRegtest.genesis_hash()` returns LWK's +hardcoded `GENESIS_LIQUID_REGTEST` constant. But `lwk_test_util::TestEnv` +starts a fresh `elementsd` instance that generates a **unique** genesis block +each time. The admin signature was computed with the wrong genesis hash. + +The swap path works despite the wrong genesis hash because both the local +env and `elementsd` use their own genesis bytes consistently — the sighash +is self-consistent within each implementation. The admin path breaks because +it explicitly includes `genesis_block_hash()` in the admin signature message. + +**Fix:** Added `DeadcatSdk::set_chain_genesis_hash()` and +`chain_genesis_hash()`. The integration test fixture fetches the real genesis +hash via `elementsd` RPC (`getblockhash 0`) and sets it on the node before +any admin operations. + +**Design decision:** Why not fetch from Electrum at runtime? The test's +`electrs` process can't handle additional TCP connections while the SDK has +an open one. `server.features` calls consistently fail. For production +(Liquid / Liquid Testnet), the hardcoded genesis hashes are correct. Only +regtest needs the override, and the test framework has RPC access to provide +it. + +**For production:** On Liquid mainnet and testnet, the hardcoded genesis +hashes in LWK match the real chain. `set_chain_genesis_hash` is only needed +for regtest. + +--- + +## Bug 3: Explicit outputs with confidential inputs + +**Symptom:** After fixing bugs 1-2, the increase-reserves test failed with +`"bad-txns-in-ne-out, value in != value out"`. + +**Root cause:** When increasing reserves, the wallet provides confidential +L-BTC inputs for the extra collateral + fee. All outputs were explicit. On +Elements, confidential input values are Pedersen commitments that don't +participate in the explicit value balance check. The explicit inputs (50k + +50k + 100k = 200k) don't cover the explicit outputs (60k + 60k + 110k + +500 + change = 240.5k) because the extra 40.5k comes from confidential +wallet inputs. + +**Fix:** Wallet-surplus change outputs are now blinded using `blind_last` +before the admin signature is computed. This makes the transaction use +proper CT commitments for wallet-derived value flows while keeping reserve +outputs explicit (as the covenant requires). + +**Design decision:** Reserve-surplus change outputs (from decreasing reserves) +stay explicit. Their source inputs are explicit covenant outputs — the blinder +can't produce surjection proofs without confidential inputs for those assets. +This is fine because the reserve amounts are already public on-chain. + +For the decrease-only case (no wallet inputs), the fee is absorbed from the +explicit collateral surplus (`fee_absorbed_by_collateral_surplus`), avoiding +any confidential value flows entirely. + +--- + +## Bug 4: Blinding input index mismatch + +**Symptom:** After fixing bug 3, the increase test still failed with +`"value in != value out"`. + +**Root cause:** `blind_order_pset` indexes `wallet_inputs` starting at 0, +but in the PSET, wallet inputs are at indices 3+ (after the 3 reserve +inputs at indices 0-2). `blind_last` looks up input secrets by PSET index. +When building the surjection proof for a wallet-surplus change output, it +looks for the wallet input at PSET index 3 but the secrets map only has +indices 0-2 (the first 3 wallet inputs mapped to the wrong positions). + +**Fix:** `adjust_lmsr_pool` builds the `inp_txout_sec` map manually with +correct PSET indices: reserve inputs at 0-2, wallet inputs at 3+. This +bypasses `blind_order_pset` for the adjust case. + +**Note:** `blind_order_pset` works correctly for its other callers +(`build_lmsr_bootstrap_pset`, limit orders) because in those cases ALL +inputs are wallet inputs, so the 0-based enumeration matches the PSET +indices. + +--- + +## Simplicity evaluation architecture + +Three distinct evaluation paths exist, and understanding their differences +was critical for debugging: + +### Rust `BitMachine` (local, pre-broadcast check) + +- In `simplicity-lang` (`src/bit_machine/mod.rs`) +- Control flow (case/comp/disconnect) in **Rust** +- Individual jets called via **C FFI** into `simplicity-sys` +- Used by the `#[cfg(debug_assertions)]` check in `attach_lmsr_pool_witnesses` +- **Limitation:** Cannot detect genesis hash mismatches with `elementsd`, + because both the signature and the env use the same (possibly wrong) bytes + +### C evaluator via corrected FFI (`c_eval::run_program_with_env`) + +- Uses `simplicity-sys`'s bundled C code (`eval.c`) +- Entire program evaluation (control flow + jets) in **C** +- Called via our corrected FFI binding in `lmsr_pool/c_eval.rs` +- **Note:** `simplicity-sys` ≤0.6.2 has a bug in its `evalTCOExpression` + binding (missing `minCost` parameter). Our `c_eval.rs` works around this + with a corrected `extern "C"` declaration. + +### `elementsd` on-chain evaluation + +- Same C code as above, but `elementsd` constructs the `txEnv` from its + internal `CTransaction` + `PrecomputedTransactionData` +- The genesis hash comes from `uint256::data()` which returns the real + chain genesis in internal byte order +- **This is the consensus reference.** If the Rust evaluator and C evaluator + both accept a program but `elementsd` rejects it, the issue is in how the + `txEnv` is constructed, not in the program itself. + +### What the unit test covers + +`admin_adjust_c_evaluator_agrees_with_rust` runs both the Rust BitMachine +AND the C evaluator (via `c_eval::run_program_with_env`) against the same +`ElementsEnv`. This catches: + +- Witness serialization bugs (C deserializer rejects) +- Pruning/value corruption bugs (C evaluator disagrees with Rust) +- Admin signature computation errors (jet assertion failures) + +It does **not** catch genesis hash mismatches with `elementsd` (both +evaluators use the same locally-constructed env). That's what the +integration tests are for. + +--- + +## `simplicity-sys` FFI bug (known issue, ≤0.6.2) + +The `evalTCOExpression` Rust binding in `simplicity-sys/src/tests/ffi.rs` +is missing the `minCost: ubounded` parameter, shifting all subsequent +arguments. This makes `simplicity_sys::tests::run_program` with +`TestUpTo::Everything` and a non-null env produce undefined behavior. + +Our `c_eval.rs` module works around this with a corrected `extern "C"` +declaration that includes `min_cost`. + +Additionally, `run_program`'s budget handling casts a `u32` value to a raw +pointer (`budget.map(|b| b as *const _)`) instead of taking a reference. + +Both bugs are in the test-utilities module only, not in the jet FFI used by +`BitMachine` at runtime. + +--- + +## Dependency patches + +The workspace `Cargo.toml` patches `simplicity-lang` and `simplicity-sys` +to point to upstream `BlockstreamResearch/rust-simplicity` master at commit +`8839c919` (post-PR-#348, pre-jet-rename). This includes the +`Value::right_shift_1` bit corruption fix from PR #348 which prevents +corrupted Left/Right tag bits during pruning. Once `simplicity-lang` ≥0.7.1 +is released with the fix, the patches can be removed. + +The SDK also has an optional dependency on `simplicity-sys` with the +`test-utils` feature, gated behind `feature = "testing"`, to enable the +in-process C evaluator check. diff --git a/src-tauri/crates/deadcat-sdk/docs/simplicity-test-methodology.md b/src-tauri/crates/deadcat-sdk/docs/simplicity-test-methodology.md new file mode 100644 index 00000000..5ba81ea8 --- /dev/null +++ b/src-tauri/crates/deadcat-sdk/docs/simplicity-test-methodology.md @@ -0,0 +1,139 @@ +# Simplicity Testing Methodology + +**Applies to:** LMSR pool covenant operations (create, scan, swap, adjust, close) + +--- + +## Three evaluation tiers + +Simplicity programs are verified at three levels, each catching different +classes of bugs with different cost/speed tradeoffs. + +### Tier 1: Rust BitMachine (unit tests, ~1-2s) + +**What it is:** The `simplicity-lang` Rust evaluator. Control flow in Rust, +individual jets via C FFI into `simplicity-sys`. + +**What it catches:** +- Wrong witness values (bad signature, wrong proof, missing fields) +- Contract assertion failures (fee inequality, reserve floors, delta rules) +- Merkle proof validation errors +- Pruning errors that produce structurally invalid programs + +**What it misses:** +- `ElementsEnv` construction bugs (genesis hash, txHash, tapEnvHash) +- PSET construction bugs (value balance, blinding, output ordering) +- Serialization bugs where the Rust evaluator and C deserializer disagree + +**When to use:** Any new Simplicity contract logic, witness field, or +signature scheme. This is the default — if you can test it here, do. + +**How:** Construct a mock PSET with `explicit_txout` + `dummy_utxo`, build +an `ElementsEnv`, call `satisfy_*_with_env`, run `BitMachine::exec`. +See `admin_adjust_c_evaluator_agrees_with_rust` in `assembly.rs` for the +pattern. + +### Tier 1.5: C evaluator via corrected FFI (unit tests, ~2s) + +**What it is:** The full C `evalTCOExpression` pipeline (decode → type-infer +→ fill witness → analyse bounds → execute) called via `c_eval.rs`. + +**What it catches:** Everything Tier 1 catches, plus: +- Serialization roundtrip bugs (Rust serializer → C deserializer) +- Pruning bugs where the Rust and C evaluators disagree on branch selection +- Value bit corruption (the `right_shift_1` bug from PR #348) + +**What it misses:** +- Same `ElementsEnv` and PSET construction gaps as Tier 1 +- Currently uses `simplicity-sys`'s bundled C code, which may differ from + `elementsd`'s version (though in practice they're identical) + +**When to use:** Alongside Tier 1 for any path that involves pruning +(admin path prunes the swap branch and vice versa). The overhead is +negligible — include it whenever the `testing` feature is enabled. + +**How:** After `BitMachine::exec`, call +`crate::lmsr_pool::c_eval::run_program_with_env(&prog_bytes, &wit_bytes, env.c_tx_env())`. +This is already wired into `attach_lmsr_pool_witnesses` under +`#[cfg(feature = "testing")]`. + +### Tier 2: Regtest integration tests (~20-30s each) + +**What it is:** A real `elementsd` + `electrs` processing the actual +broadcast transaction. + +**What it catches:** Everything, including: +- `ElementsEnv` construction divergence (genesis hash byte order, txHash + computation, tapEnvHash) +- PSET value balance (explicit vs confidential mixing) +- Blinding (surjection proofs, input index mapping) +- Fee handling (absorption from surplus, separate wallet inputs) +- `sign_pset` / `finalize` mutations +- Chain walk / rescan correctness + +**What it misses:** Nothing — this is the consensus reference. + +**When to use:** +- New transaction construction logic (PSET layout, blinding, fee handling) +- Any code that reads `ElementsEnv` jets and uses those values externally + (admin signatures, attestations) +- End-to-end flows (create → scan → trade → rescan) +- One-time verification that a Tier 1 test's mock env matches reality + +**How:** Use the `Fixture` in `tests/lmsr_pool.rs`. Call +`set_chain_genesis_hash` from the RPC. See the `bootstrap_admin_pool` + +`adjust_lmsr_pool_*` tests for the pattern. + +--- + +## Decision tree + +``` +Is the bug in Simplicity program logic (witnesses, signatures, proofs)? + YES → Tier 1 unit test (BitMachine) + + Tier 1.5 if pruning is involved + +Is the bug in PSET construction (outputs, blinding, fee, value balance)? + YES → Tier 1 unit test for the PSET structure (assert output count, + values, asset IDs, which indices are blinded) + + Tier 2 integration test for consensus acceptance + +Is the bug in ElementsEnv construction (genesis hash, sighash)? + YES → Tier 2 integration test (only way to compare against elementsd) + +Does the code path only use pure Rust validation (no chain, no Simplicity)? + YES → Regular unit test (no BitMachine, no regtest) +``` + +--- + +## Regtest genesis hash + +Each `elementsd` regtest instance has a unique genesis block hash. The SDK's +`Network::LiquidRegtest.genesis_hash()` returns a hardcoded constant that +does NOT match. For any test that exercises the admin signature path: + +1. Fetch the real genesis hash via RPC: `getblockhash 0` +2. Reverse the hex bytes (RPC returns display order, SDK uses internal order) +3. Call `node.set_chain_genesis_hash(bytes)` before any admin operations + +This is already done in the `Fixture::new()` setup. For Liquid mainnet and +testnet, the hardcoded values are correct. + +--- + +## Current coverage + +| Code path | Tier 1 | Tier 1.5 | Tier 2 | Tests | +|---|---|---|---|---| +| Swap (primary witness) | ✓ | ✓ | ✓ | `swap_primary_executes_in_bitmachine`, `create_scan_*_regtest` | +| Swap (secondary witness) | ✓ | — | ✓ | `swap_secondary_executes_in_bitmachine`, `create_scan_*_regtest` | +| Admin adjust (signature + witness) | ✓ | ✓ | ✓ | `admin_adjust_c_evaluator_agrees_with_rust`, `adjust_*_regtest` | +| Admin adjust (PSET: decrease) | ✓ | — | ✓ | `admin_adjust_pset_decrease_structure`, `adjust_*_decreases_regtest` | +| Admin adjust (PSET: increase) | — | — | ✓ | `adjust_*_increases_regtest` | +| Close (reclaimed amounts) | ✓ | — | — | `close_reclaimed_amounts` | +| Pool creation | — | — | ✓ | `create_scan_*_regtest` | +| Pool scanning / chain walk | — | — | ✓ | `create_scan_*_regtest`, `scan_*_bootstraps_*_regtest` | +| Validation rejections (IDs, bounds) | — | — | ✓ | `scan_*_rejects_*_regtest` | +| Swap fee inequality | ✓ | — | — | `rejects_buy_leg_that_violates_fee_inequality` | +| Merkle proof validation | ✓ | — | — | `rejects_leg_with_invalid_merkle_proof`, `rejects_*_missing_*` | diff --git a/src-tauri/crates/deadcat-sdk/src/lib.rs b/src-tauri/crates/deadcat-sdk/src/lib.rs index fcdfc105..ee69fd8d 100644 --- a/src-tauri/crates/deadcat-sdk/src/lib.rs +++ b/src-tauri/crates/deadcat-sdk/src/lib.rs @@ -60,6 +60,7 @@ pub use maker_order::params::{ // ── LMSR pools ───────────────────────────────────────────────────── pub use lmsr_pool::api::{ + AdjustLmsrPoolRequest, AdjustLmsrPoolResult, CloseLmsrPoolRequest, CloseLmsrPoolResult, CreateLmsrPoolRequest, CreateLmsrPoolResult, LmsrPoolLocator, LmsrPoolSnapshot, build_pool_announcement_from_snapshot, }; @@ -70,7 +71,8 @@ pub use lmsr_pool::math::{ }; pub use lmsr_pool::params::{LmsrInitialOutpoint, LmsrPoolId, LmsrPoolIdInput, LmsrPoolParams}; pub use lmsr_pool::table::{ - LmsrTableManifest, lmsr_table_leaf_hash, lmsr_table_node_hash, lmsr_table_root, + LmsrTableManifest, generate_lmsr_table, lmsr_table_leaf_hash, lmsr_table_node_hash, + lmsr_table_root, }; // ── Pool helpers ─────────────────────────────────────────────────── diff --git a/src-tauri/crates/deadcat-sdk/src/lmsr_pool/api.rs b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/api.rs index 0f9303dc..fe1078da 100644 --- a/src-tauri/crates/deadcat-sdk/src/lmsr_pool/api.rs +++ b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/api.rs @@ -8,6 +8,7 @@ use crate::lmsr_pool::params::{LmsrInitialOutpoint, LmsrPoolId, LmsrPoolParams}; use crate::lmsr_pool::table::LmsrTableManifest; use crate::pool::PoolReserves; use crate::prediction_market::params::{MarketId, PredictionMarketParams}; +use crate::trade::types::LmsrPoolUtxos; /// LMSR scan inputs plus the caller-supplied pool identity hint. /// @@ -57,6 +58,53 @@ pub struct CreateLmsrPoolResult { pub announcement: PoolAnnouncement, } +/// Request for adjusting an existing LMSR pool's reserves (AdminAdjust transition). +/// +/// Pool parameters are carried inside `locator.params`. +#[derive(Debug, Clone)] +#[allow(private_interfaces)] // LmsrPoolUtxos is pub(crate); callers use node-layer APIs +pub struct AdjustLmsrPoolRequest { + pub locator: LmsrPoolLocator, + pub current_pool_utxos: LmsrPoolUtxos, + pub current_s_index: u64, + pub current_reserves: PoolReserves, + pub new_reserves: PoolReserves, + pub table_values: Vec, + pub fee_amount: u64, + pub pool_index: u32, +} + +/// Result returned after a successful LMSR pool adjustment. +#[derive(Debug, Clone)] +pub struct AdjustLmsrPoolResult { + pub txid: Txid, + pub new_snapshot: LmsrPoolSnapshot, +} + +/// Request for closing an LMSR pool (AdminAdjust to minimum reserves). +/// +/// Pool parameters are carried inside `locator.params`. +#[derive(Debug, Clone)] +#[allow(private_interfaces)] // LmsrPoolUtxos is pub(crate); callers use node-layer APIs +pub struct CloseLmsrPoolRequest { + pub locator: LmsrPoolLocator, + pub current_pool_utxos: LmsrPoolUtxos, + pub current_s_index: u64, + pub current_reserves: PoolReserves, + pub table_values: Vec, + pub fee_amount: u64, + pub pool_index: u32, +} + +/// Result returned after a successful LMSR pool close. +#[derive(Debug, Clone)] +pub struct CloseLmsrPoolResult { + pub txid: Txid, + pub reclaimed_yes: u64, + pub reclaimed_no: u64, + pub reclaimed_collateral: u64, +} + impl TryFrom<&PoolAnnouncement> for LmsrPoolLocator { type Error = String; diff --git a/src-tauri/crates/deadcat-sdk/src/lmsr_pool/assembly.rs b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/assembly.rs index 93985a4e..dd0de5b0 100644 --- a/src-tauri/crates/deadcat-sdk/src/lmsr_pool/assembly.rs +++ b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/assembly.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use lwk_wollet::elements::pset::PartiallySignedTransaction; use simplicityhl::elements::hashes::Hash; use simplicityhl::elements::taproot::ControlBlock; +use simplicityhl::simplicity::bit_machine::BitMachine; use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; use crate::assembly::pset_to_pruning_transaction; @@ -23,6 +24,7 @@ pub fn attach_lmsr_pool_witnesses( pset: &mut PartiallySignedTransaction, leg: &LmsrPoolSwapLeg, lmsr_input_range: Range, + genesis_hash: [u8; 32], ) -> Result<()> { let input_start = lmsr_input_range.start; let input_end = lmsr_input_range.end; @@ -112,9 +114,34 @@ pub fn attach_lmsr_pool_witnesses( cmr, control_block, None, - lwk_wollet::elements::BlockHash::all_zeros(), + lwk_wollet::elements::BlockHash::from_byte_array(genesis_hash), ); let satisfied = satisfy_lmsr_primary_with_env(&contract, leg, Some(&env))?; + + // Verify the satisfied program executes in the Rust BitMachine. + // satisfy_with_env only prunes — it does NOT run jets. + #[cfg(debug_assertions)] + { + let redeem = satisfied.redeem(); + let mut machine = BitMachine::for_program(redeem) + .map_err(|e| Error::Witness(format!("lmsr primary BitMachine init: {e}")))?; + machine + .exec(redeem, &env) + .map_err(|e| Error::Witness(format!("lmsr primary local execution failed: {e}")))?; + } + + // Run the full C evaluator pipeline (decode → type-infer → execute) + // against the same env. This matches what elementsd does and catches + // pruning/serialization bugs that the Rust BitMachine misses. + #[cfg(feature = "testing")] + { + let (prog_bytes, wit_bytes) = serialize_satisfied(&satisfied); + crate::lmsr_pool::c_eval::run_program_with_env(&prog_bytes, &wit_bytes, env.c_tx_env()) + .map_err(|e| { + Error::Witness(format!("C evaluator rejected lmsr primary program: {e}")) + })?; + } + let (program_bytes, witness_bytes) = serialize_satisfied(&satisfied); let cmr_bytes = cmr.to_byte_array().to_vec(); let stack = vec![witness_bytes, program_bytes, cmr_bytes, cb_bytes]; @@ -137,7 +164,7 @@ pub fn attach_lmsr_pool_witnesses( cmr, control_block, None, - lwk_wollet::elements::BlockHash::all_zeros(), + lwk_wollet::elements::BlockHash::from_byte_array(genesis_hash), ); let satisfied = satisfy_lmsr_secondary_with_env(&contract, leg.in_base, Some(&env))?; let (program_bytes, witness_bytes) = serialize_satisfied(&satisfied); @@ -162,6 +189,8 @@ pub fn attach_lmsr_pool_witnesses( #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use crate::lmsr_pool::math::LmsrTradeKind; use crate::lmsr_pool::params::LmsrPoolParams; @@ -278,7 +307,7 @@ mod tests { add_pset_input(&mut pset, &leg.pool_utxos.no); add_pset_input(&mut pset, &leg.pool_utxos.collateral); - let err = attach_lmsr_pool_witnesses(&mut pset, &leg, 0..2).unwrap_err(); + let err = attach_lmsr_pool_witnesses(&mut pset, &leg, 0..2, [0; 32]).unwrap_err(); assert!( err.to_string() .contains("requires exactly 3 covenant inputs") @@ -290,7 +319,7 @@ mod tests { let leg = test_leg(); let mut pset = build_swap_pset(&leg); - attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3).unwrap(); + attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3, [0; 32]).unwrap(); assert!(pset.inputs()[0].final_script_witness.is_some()); assert!(pset.inputs()[1].final_script_witness.is_some()); assert!(pset.inputs()[2].final_script_witness.is_some()); @@ -302,7 +331,7 @@ mod tests { leg.delta_in = 1_000; // below required min collateral-in for (old_f,new_f,fee_bps) let mut pset = build_swap_pset(&leg); - assert!(attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3).is_err()); + assert!(attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3, [0; 32]).is_err()); } #[test] @@ -310,7 +339,7 @@ mod tests { let mut leg = test_leg(); leg.old_siblings[0][0] ^= 0x01; let mut pset = build_swap_pset(&leg); - assert!(attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3).is_err()); + assert!(attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3, [0; 32]).is_err()); } #[test] @@ -319,7 +348,7 @@ mod tests { let _ = leg.old_siblings.pop(); let _ = leg.new_siblings.pop(); let mut pset = build_swap_pset(&leg); - assert!(attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3).is_err()); + assert!(attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3, [0; 32]).is_err()); } #[test] @@ -327,6 +356,365 @@ mod tests { let mut leg = test_leg(); leg.pool_params.s_max_index = 8; let mut pset = build_swap_pset(&leg); - assert!(attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3).is_err()); + assert!(attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3, [0; 32]).is_err()); + } + + // ── Tier 1: BitMachine execution tests ───────────────────────────── + + /// Helper: build an ElementsEnv from a PSET for a given input index. + fn build_env( + pset: &PartiallySignedTransaction, + contract: &CompiledLmsrPool, + s_index: u64, + input_index: u32, + is_primary: bool, + ) -> simplicityhl::simplicity::jet::elements::ElementsEnv> + { + use simplicityhl::elements::taproot::ControlBlock; + use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; + + let tx = Arc::new(crate::assembly::pset_to_pruning_transaction(pset).unwrap()); + let utxos: Vec = pset + .inputs() + .iter() + .map(|inp| ElementsUtxo::from(inp.witness_utxo.clone().unwrap())) + .collect(); + let (cmr, cb_bytes) = if is_primary { + ( + *contract.primary_cmr(), + contract.primary_control_block(s_index), + ) + } else { + ( + *contract.secondary_cmr(), + contract.secondary_control_block(s_index), + ) + }; + let control_block = ControlBlock::from_slice(&cb_bytes).unwrap(); + ElementsEnv::new( + tx, + utxos, + input_index, + cmr, + control_block, + None, + lwk_wollet::elements::BlockHash::from_byte_array([0u8; 32]), + ) + } + + /// Swap path: run BitMachine on the primary input. + #[test] + fn swap_primary_executes_in_bitmachine() { + use crate::lmsr_pool::witness::satisfy_lmsr_primary_with_env; + + let leg = test_leg(); + let pset = build_swap_pset(&leg); + let contract = CompiledLmsrPool::new(leg.pool_params).unwrap(); + let env = build_env(&pset, &contract, leg.old_s_index, 0, true); + + let satisfied = + satisfy_lmsr_primary_with_env(&contract, &leg, Some(&env)).expect("satisfy primary"); + let redeem = satisfied.redeem(); + let mut machine = BitMachine::for_program(redeem).expect("BitMachine init"); + machine + .exec(redeem, &env) + .expect("swap primary should pass BitMachine"); + } + + /// Swap path: run BitMachine on secondary inputs (NO slot and collateral slot). + #[test] + fn swap_secondary_executes_in_bitmachine() { + use crate::lmsr_pool::witness::satisfy_lmsr_secondary_with_env; + + let leg = test_leg(); + let pset = build_swap_pset(&leg); + let contract = CompiledLmsrPool::new(leg.pool_params).unwrap(); + + // Secondary input 1 (NO slot, PSET index 1) + let env_no = build_env(&pset, &contract, leg.old_s_index, 1, false); + let satisfied_no = satisfy_lmsr_secondary_with_env(&contract, leg.in_base, Some(&env_no)) + .expect("satisfy secondary NO"); + let redeem_no = satisfied_no.redeem(); + let mut machine_no = BitMachine::for_program(redeem_no).expect("BitMachine init NO"); + machine_no + .exec(redeem_no, &env_no) + .expect("swap secondary NO should pass BitMachine"); + + // Secondary input 2 (collateral slot, PSET index 2) + let env_coll = build_env(&pset, &contract, leg.old_s_index, 2, false); + let satisfied_coll = + satisfy_lmsr_secondary_with_env(&contract, leg.in_base, Some(&env_coll)) + .expect("satisfy secondary collateral"); + let redeem_coll = satisfied_coll.redeem(); + let mut machine_coll = BitMachine::for_program(redeem_coll).expect("BitMachine init coll"); + machine_coll + .exec(redeem_coll, &env_coll) + .expect("swap secondary collateral should pass BitMachine"); + } + + // ── PSET construction tests ────────────────────────────────────────── + + /// Verify admin adjust PSET structure for the decrease (no wallet inputs) case. + #[test] + fn admin_adjust_pset_decrease_structure() { + let yes = [0x11; 32]; + let no = [0x22; 32]; + let collateral = [0x33; 32]; + let table_values: Vec = (0..16u64).map(|i| 2_000 + i * 10).collect(); + let table_root = lmsr_table_root(&table_values).unwrap(); + let params = LmsrPoolParams { + yes_asset_id: yes, + no_asset_id: no, + collateral_asset_id: collateral, + lmsr_table_root: table_root, + table_depth: 4, + q_step_lots: 10, + s_bias: 8, + s_max_index: 15, + half_payout_sats: 1_000, + fee_bps: 30, + min_r_yes: 1, + min_r_no: 1, + min_r_collateral: 1, + cosigner_pubkey: crate::taproot::NUMS_KEY_BYTES, + }; + let contract = CompiledLmsrPool::new(params).unwrap(); + let spk = contract.script_pubkey(8); + + // Decrease: 50k→40k YES, 50k→40k NO, 100k→90k collateral, fee=500 + let mut pset = new_pset(); + add_pset_input(&mut pset, &dummy_utxo(yes, 50_000, 0, &spk)); + add_pset_input(&mut pset, &dummy_utxo(no, 50_000, 1, &spk)); + add_pset_input(&mut pset, &dummy_utxo(collateral, 100_000, 2, &spk)); + + // Reserve outputs + add_pset_output(&mut pset, explicit_txout(&yes, 40_000, &spk)); + add_pset_output(&mut pset, explicit_txout(&no, 40_000, &spk)); + add_pset_output(&mut pset, explicit_txout(&collateral, 90_000, &spk)); + // Fee (absorbed from collateral surplus: 10k surplus - 500 fee = 9.5k change) + add_pset_output(&mut pset, explicit_txout(&collateral, 500, &Script::new())); + // Change outputs (explicit, not blinded) + add_pset_output(&mut pset, explicit_txout(&yes, 10_000, &Script::new())); + add_pset_output(&mut pset, explicit_txout(&no, 10_000, &Script::new())); + add_pset_output( + &mut pset, + explicit_txout(&collateral, 9_500, &Script::new()), + ); + + assert_eq!(pset.inputs().len(), 3, "3 reserve inputs"); + assert_eq!(pset.outputs().len(), 7, "3 reserves + fee + 3 change"); + + // Verify explicit value balance + let total_in: u64 = 50_000 + 50_000 + 100_000; + let total_out: u64 = 40_000 + 40_000 + 90_000 + 500 + 10_000 + 10_000 + 9_500; + assert_eq!(total_in, total_out, "value must balance"); + + // No outputs should be blinded (all explicit for the zero-wallet-inputs case) + for (i, out) in pset.outputs().iter().enumerate() { + assert!( + out.blinding_key.is_none(), + "output {i} should not be blinded in the decrease case" + ); + } + } + + /// Verify close_lmsr_pool reclaimed amounts match expectations. + #[test] + fn close_reclaimed_amounts() { + use crate::pool::PoolReserves; + + let min_yes = 1u64; + let min_no = 1u64; + let min_collateral = 1u64; + let current = PoolReserves { + r_yes: 50_000, + r_no: 40_000, + r_lbtc: 100_000, + }; + + let reclaimed_yes = current.r_yes.saturating_sub(min_yes); + let reclaimed_no = current.r_no.saturating_sub(min_no); + let reclaimed_collateral = current.r_lbtc.saturating_sub(min_collateral); + + assert_eq!(reclaimed_yes, 49_999); + assert_eq!(reclaimed_no, 39_999); + assert_eq!(reclaimed_collateral, 99_999); + } + + // ── Tier 1 + 1.5: BitMachine + C evaluator ────────────────────────── + + /// Build an admin-adjust leg and run both Rust BitMachine and C evaluator. + /// + /// This is a fast unit test (no elementsd) that isolates Rust-vs-C + /// evaluator divergence for the admin path. + #[test] + fn admin_adjust_c_evaluator_agrees_with_rust() { + use crate::lmsr_pool::table::LmsrTableManifest; + use crate::lmsr_pool::witness::{satisfy_lmsr_primary_with_env, serialize_satisfied}; + use lwk_wollet::elements::secp256k1_zkp::{self, Keypair, Secp256k1}; + use sha2::{Digest, Sha256}; + use std::sync::Arc; + + // 1. Build params with a real admin cosigner + let secp = Secp256k1::new(); + let secret = secp256k1_zkp::SecretKey::from_slice(&[0x42; 32]).unwrap(); + let admin_keypair = Keypair::from_secret_key(&secp, &secret); + let (admin_xonly, _) = admin_keypair.x_only_public_key(); + + let yes = [0x11; 32]; + let no = [0x22; 32]; + let collateral = [0x33; 32]; + let table_values: Vec = (0..16u64).map(|i| 2_000 + i * 10).collect(); + let table_root = lmsr_table_root(&table_values).unwrap(); + let manifest = LmsrTableManifest::new(4, table_values.clone()).unwrap(); + let proof = manifest.proof_at(8).unwrap(); + let params = LmsrPoolParams { + yes_asset_id: yes, + no_asset_id: no, + collateral_asset_id: collateral, + lmsr_table_root: table_root, + table_depth: 4, + q_step_lots: 10, + s_bias: 8, + s_max_index: 15, + half_payout_sats: 1_000, + fee_bps: 30, + min_r_yes: 1, + min_r_no: 1, + min_r_collateral: 1, + cosigner_pubkey: admin_xonly.serialize(), + }; + let contract = CompiledLmsrPool::new(params).unwrap(); + let spk = contract.script_pubkey(8); + + // 2. Build a no-op admin adjust: same reserves in and out, same s_index + let in_yes = 50_000u64; + let in_no = 50_000u64; + let in_collateral = 100_000u64; + + let pool_utxos = LmsrPoolUtxos { + yes: dummy_utxo(yes, in_yes, 0, &spk), + no: dummy_utxo(no, in_no, 1, &spk), + collateral: dummy_utxo(collateral, in_collateral, 2, &spk), + }; + + // 3. Build PSET: 3 reserve inputs → 3 reserve outputs (same values) + fee + let mut pset = new_pset(); + add_pset_input(&mut pset, &pool_utxos.yes); + add_pset_input(&mut pset, &pool_utxos.no); + add_pset_input(&mut pset, &pool_utxos.collateral); + + // Reserve outputs (unchanged) + add_pset_output(&mut pset, explicit_txout(&yes, in_yes, &spk)); + add_pset_output(&mut pset, explicit_txout(&no, in_no, &spk)); + add_pset_output( + &mut pset, + explicit_txout(&collateral, in_collateral - 500, &spk), + ); + // Fee output + add_pset_output(&mut pset, explicit_txout(&collateral, 500, &Script::new())); + + // 4. Compute admin signature + let genesis_hash = [0u8; 32]; // test genesis + let tx = Arc::new(crate::assembly::pset_to_pruning_transaction(&pset).unwrap()); + let utxos: Vec = pset + .inputs() + .iter() + .map(|inp| { + simplicityhl::simplicity::jet::elements::ElementsUtxo::from( + inp.witness_utxo.clone().unwrap(), + ) + }) + .collect(); + let primary_cmr = *contract.primary_cmr(); + let cb_bytes = contract.primary_control_block(8); + let control_block = + simplicityhl::elements::taproot::ControlBlock::from_slice(&cb_bytes).unwrap(); + let env = simplicityhl::simplicity::jet::elements::ElementsEnv::new( + tx, + utxos, + 0, + primary_cmr, + control_block, + None, + lwk_wollet::elements::BlockHash::from_byte_array(genesis_hash), + ); + let sig_all: [u8; 32] = env.c_tx_env().sighash_all().to_byte_array(); + + let mut hasher = Sha256::new(); + hasher.update(b"DEADCAT/LMSR_LIQUIDITY_ADJUST_V1"); + hasher.update(genesis_hash); + hasher.update(params.lmsr_table_root); + hasher.update(params.yes_asset_id); + hasher.update(params.no_asset_id); + hasher.update(params.collateral_asset_id); + hasher.update(sig_all); + // Input prevouts + hasher.update(pool_utxos.yes.outpoint.txid.to_byte_array()); + hasher.update(pool_utxos.yes.outpoint.vout.to_be_bytes()); + hasher.update(pool_utxos.no.outpoint.txid.to_byte_array()); + hasher.update(pool_utxos.no.outpoint.vout.to_be_bytes()); + hasher.update(pool_utxos.collateral.outpoint.txid.to_byte_array()); + hasher.update(pool_utxos.collateral.outpoint.vout.to_be_bytes()); + // Indices + hasher.update(0u32.to_be_bytes()); // i_yes = in_base + hasher.update(0u32.to_be_bytes()); // out_base + hasher.update(8u64.to_be_bytes()); // old_s_index + hasher.update(8u64.to_be_bytes()); // new_s_index + // Reserves + hasher.update(in_yes.to_be_bytes()); + hasher.update(in_no.to_be_bytes()); + hasher.update(in_collateral.to_be_bytes()); + hasher.update(in_yes.to_be_bytes()); // out = in (no-op) + hasher.update(in_no.to_be_bytes()); + hasher.update((in_collateral - 500).to_be_bytes()); // minus fee + // SPK hashes + let spk_hash = contract.script_hash(8); + hasher.update(spk_hash); + hasher.update(spk_hash); + hasher.update(spk_hash); + + let msg_hash: [u8; 32] = hasher.finalize().into(); + let msg = secp256k1_zkp::Message::from_digest(msg_hash); + let admin_sig = secp.sign_schnorr_no_aux_rand(&msg, &admin_keypair); + + // 5. Build the swap leg + let leg = LmsrPoolSwapLeg { + primary_path: LmsrPrimaryPath::AdminAdjust, + pool_params: params, + pool_id: "test-admin-pool".into(), + old_s_index: 8, + new_s_index: 8, + old_path_bits: proof.path_bits, + new_path_bits: proof.path_bits, + old_siblings: proof.siblings.clone(), + new_siblings: proof.siblings, + in_base: 0, + out_base: 0, + pool_utxos, + trade_kind: LmsrTradeKind::BuyYes, // unused for admin + old_f: table_values[8], + new_f: table_values[8], + delta_in: 0, + delta_out: 0, + admin_signature: admin_sig.serialize(), + }; + + // 6. Satisfy and run Rust BitMachine + let satisfied = + satisfy_lmsr_primary_with_env(&contract, &leg, Some(&env)).expect("satisfy"); + let redeem = satisfied.redeem(); + let mut machine = BitMachine::for_program(redeem).expect("BitMachine init"); + machine + .exec(redeem, &env) + .expect("Rust BitMachine should accept admin adjust"); + + // 7. Run the full C evaluator to match what elementsd does + #[cfg(feature = "testing")] + { + let (prog_bytes, wit_bytes) = serialize_satisfied(&satisfied); + crate::lmsr_pool::c_eval::run_program_with_env(&prog_bytes, &wit_bytes, env.c_tx_env()) + .expect("C evaluator should accept admin adjust program"); + } } } diff --git a/src-tauri/crates/deadcat-sdk/src/lmsr_pool/c_eval.rs b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/c_eval.rs new file mode 100644 index 00000000..d5ab043d --- /dev/null +++ b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/c_eval.rs @@ -0,0 +1,244 @@ +//! Direct C Simplicity evaluator with corrected FFI signatures. +//! +//! `simplicity-sys` ≤0.6.2 has a bug in its `evalTCOExpression` Rust binding: +//! the `minCost: ubounded` parameter is missing, shifting all subsequent args. +//! This module declares the correct signatures and provides a minimal +//! `run_program_with_env` that matches what `elementsd` does. +//! +//! Remove this module once `simplicity-sys` is updated with the fix (PR #355). + +#![cfg(feature = "testing")] + +use std::os::raw::{c_int, c_uchar, c_uint}; +use std::ptr; + +use simplicity_sys::CElementsTxEnv; + +// --- Raw C types matching simplicity-sys internals --- + +type Ubounded = u32; +type CSize = usize; +const UBOUNDED_MAX: Ubounded = u32::MAX; + +#[repr(C)] +struct CBitstream { + arr: *const c_uchar, + len: CSize, + offset: CSize, +} + +impl CBitstream { + fn from_slice(s: &[u8]) -> Self { + CBitstream { + arr: s.as_ptr(), + len: s.len(), + offset: 0, + } + } +} + +// Opaque types — we only pass pointers to them. +#[repr(C)] +struct CDagNode { + _opaque: [u8; 0], +} + +#[repr(C)] +struct CType { + _opaque: [u8; 0], +} + +#[repr(C)] +#[derive(Default)] +struct CCombinatorCounters { + comp_cnt: usize, + case_cnt: usize, + pair_cnt: usize, + disconnect_cnt: usize, + injl_cnt: usize, + injr_cnt: usize, + take_cnt: usize, + drop_cnt: usize, +} + +type DecodeJetFn = unsafe extern "C" fn(*mut CDagNode, CSize, *mut CBitstream) -> c_int; + +unsafe extern "C" { + #[link_name = "rustsimplicity_0_6_decodeMallocDag"] + fn decodeMallocDag( + dag: *mut *mut CDagNode, + decode_jet: DecodeJetFn, + census: *mut CCombinatorCounters, + stream: *mut CBitstream, + ) -> c_int; + + #[link_name = "rustsimplicity_0_6_closeBitstream"] + fn closeBitstream(stream: *mut CBitstream) -> c_int; + + #[link_name = "rustsimplicity_0_6_elements_decodeJet"] + fn elements_decodeJet(dag: *mut CDagNode, i: CSize, stream: *mut CBitstream) -> c_int; + + #[link_name = "rustsimplicity_0_6_mallocTypeInference"] + fn mallocTypeInference( + type_dag: *mut *mut CType, + malloc_bound_vars: unsafe extern "C" fn( + *mut CType, + CSize, + *const CDagNode, + CSize, + *const CCombinatorCounters, + ) -> c_int, + dag: *const CDagNode, + len: CSize, + census: *const CCombinatorCounters, + ) -> c_int; + + #[link_name = "rustsimplicity_0_6_elements_mallocBoundVars"] + fn elements_mallocBoundVars( + type_dag: *mut CType, + offset: CSize, + dag: *const CDagNode, + len: CSize, + census: *const CCombinatorCounters, + ) -> c_int; + + #[link_name = "rustsimplicity_0_6_fillWitnessData"] + fn fillWitnessData( + dag: *mut CDagNode, + type_dag: *mut CType, + len: CSize, + stream: *mut CBitstream, + ) -> c_int; + + #[link_name = "rustsimplicity_0_6_analyseBounds"] + fn analyseBounds( + cell_bound: *mut Ubounded, + uword_bound: *mut Ubounded, + frame_bound: *mut Ubounded, + cost_bound: *mut Ubounded, + max_cells: Ubounded, + min_cost: Ubounded, + max_cost: Ubounded, + dag: *const CDagNode, + type_dag: *const CType, + len: CSize, + ) -> c_int; + + /// Corrected binding — includes `min_cost` that `simplicity-sys` omits. + #[link_name = "rustsimplicity_0_6_evalTCOExpression"] + fn evalTCOExpression( + anti_dos_checks: c_uchar, + output: *mut c_uint, + input: *const c_uint, + dag: *const CDagNode, + type_dag: *mut CType, + len: CSize, + min_cost: Ubounded, + budget: *const Ubounded, + env: *const CElementsTxEnv, + ) -> c_int; +} + +fn sim_free(ptr: *mut u8) { + unsafe { simplicity_sys::alloc::rust_0_6_free(ptr) } +} + +fn check(code: c_int, label: &str) -> Result { + if code < 0 { + Err(format!("C {label} failed with code {code}")) + } else { + Ok(code as usize) + } +} + +/// Run a Simplicity program through the full C pipeline with a real env. +pub fn run_program_with_env( + program: &[u8], + witness: &[u8], + env: &CElementsTxEnv, +) -> Result<(), String> { + unsafe { + let mut prog_stream = CBitstream::from_slice(program); + let mut wit_stream = CBitstream::from_slice(witness); + let mut census = CCombinatorCounters::default(); + + // 1. Decode + let mut dag: *mut CDagNode = ptr::null_mut(); + let len = check( + decodeMallocDag(&mut dag, elements_decodeJet, &mut census, &mut prog_stream), + "decodeMallocDag", + )?; + assert!(!dag.is_null()); + struct DagGuard(*mut CDagNode); + impl Drop for DagGuard { + fn drop(&mut self) { + sim_free(self.0 as *mut u8) + } + } + let _g1 = DagGuard(dag); + + check(closeBitstream(&mut prog_stream), "closeBitstream(prog)")?; + + // 2. Type inference + let mut type_dag: *mut CType = ptr::null_mut(); + check( + mallocTypeInference(&mut type_dag, elements_mallocBoundVars, dag, len, &census), + "mallocTypeInference", + )?; + assert!(!type_dag.is_null()); + struct TypeGuard(*mut CType); + impl Drop for TypeGuard { + fn drop(&mut self) { + sim_free(self.0 as *mut u8) + } + } + let _g2 = TypeGuard(type_dag); + + // 3. Fill witness + check( + fillWitnessData(dag, type_dag, len, &mut wit_stream), + "fillWitnessData", + )?; + check(closeBitstream(&mut wit_stream), "closeBitstream(wit)")?; + + // 4. Analyse bounds + let mut cell_bound: Ubounded = 0; + let mut word_bound: Ubounded = 0; + let mut frame_bound: Ubounded = 0; + let mut cost_bound: Ubounded = 0; + check( + analyseBounds( + &mut cell_bound, + &mut word_bound, + &mut frame_bound, + &mut cost_bound, + UBOUNDED_MAX, + 0, + UBOUNDED_MAX, + dag, + type_dag, + len, + ), + "analyseBounds", + )?; + + // 5. Execute with corrected FFI + let result = evalTCOExpression( + 0xFF, // CHECK_ALL + ptr::null_mut(), + ptr::null(), + dag, + type_dag, + len, + 0, // min_cost + ptr::null(), // budget (NULL = use analysed bounds) + env, + ); + + if result == 0 { + Ok(()) + } else { + Err(format!("C evalTCOExpression returned error code {result}")) + } + } +} diff --git a/src-tauri/crates/deadcat-sdk/src/lmsr_pool/mod.rs b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/mod.rs index 485a4b6d..a2918ddd 100644 --- a/src-tauri/crates/deadcat-sdk/src/lmsr_pool/mod.rs +++ b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/mod.rs @@ -1,5 +1,7 @@ pub mod api; pub mod assembly; +#[cfg(feature = "testing")] +pub(crate) mod c_eval; pub mod chain_walk; pub mod contract; pub(crate) mod identity; diff --git a/src-tauri/crates/deadcat-sdk/src/lmsr_pool/table.rs b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/table.rs index 84afb140..6bfc0582 100644 --- a/src-tauri/crates/deadcat-sdk/src/lmsr_pool/table.rs +++ b/src-tauri/crates/deadcat-sdk/src/lmsr_pool/table.rs @@ -247,6 +247,66 @@ pub fn verify_lmsr_table_proof( Ok(()) } +/// Generate LMSR table values F(i) for a binary market. +/// +/// F(i) = floor(b * ln(exp(q_yes(i)/b) + exp(q_no(i)/b))) +/// +/// where q_yes(i) = (i as i64 - s_bias as i64) * q_step_lots * half_payout_sats +/// q_no(i) = -q_yes(i) (symmetric binary market) +/// b = liquidity_param * half_payout_sats (scaling) +pub fn generate_lmsr_table( + liquidity_param: f64, + table_depth: u32, + q_step_lots: u64, + s_bias: u64, + half_payout_sats: u64, +) -> Result> { + // Cap at 20 for generation to avoid OOM (2^20 = 1M entries × 8 bytes ≈ 8 MB). + // The manifest/proof code supports up to 63 for on-chain verification, but + // generating that many values in memory is not practical. + if table_depth == 0 || table_depth > 20 { + return Err(Error::LmsrPool(format!( + "table_depth must be in [1, 20] for generation, got {table_depth}" + ))); + } + if liquidity_param <= 0.0 || !liquidity_param.is_finite() { + return Err(Error::LmsrPool( + "liquidity_param must be a positive finite number".into(), + )); + } + if half_payout_sats == 0 { + return Err(Error::LmsrPool("half_payout_sats must be > 0".into())); + } + if q_step_lots == 0 { + return Err(Error::LmsrPool("q_step_lots must be > 0".into())); + } + + let n = 1usize << table_depth; + let b = liquidity_param * half_payout_sats as f64; + let mut values = Vec::with_capacity(n); + + for i in 0..n { + let q_yes = (i as f64 - s_bias as f64) * q_step_lots as f64 * half_payout_sats as f64; + let q_no = -q_yes; + // F(i) = floor(b * ln(exp(q_yes/b) + exp(q_no/b))) + // Use log-sum-exp trick for numerical stability: + // ln(exp(a) + exp(b)) = max(a,b) + ln(1 + exp(-|a-b|)) + let a = q_yes / b; + let a_neg = q_no / b; + let max_val = a.max(a_neg); + let lse = max_val + ((-((a - a_neg).abs())).exp()).ln_1p(); + let f_i = (b * lse).floor(); + if f_i < 0.0 || !f_i.is_finite() { + return Err(Error::LmsrPool(format!( + "LMSR table value at index {i} is invalid: {f_i}" + ))); + } + values.push(f_i as u64); + } + + Ok(values) +} + fn expected_leaf_count(table_depth: u32) -> Result { let count_u128 = 1u128 .checked_shl(table_depth) @@ -354,4 +414,115 @@ mod tests { .unwrap_err(); assert!(err.to_string().contains("root mismatch")); } + + #[test] + fn generate_lmsr_table_convexity() { + let s_bias = 8u64; + let values = generate_lmsr_table(1.0, 4, 10, s_bias, 5_000).unwrap(); + assert_eq!(values.len(), 16); + // F(i) is convex (U-shaped): decreasing before s_bias, increasing after + // Values should increase as we move away from s_bias + for i in (s_bias as usize + 1)..values.len() { + assert!( + values[i] >= values[i - 1], + "F({}) = {} < F({}) = {} (above bias)", + i, + values[i], + i - 1, + values[i - 1] + ); + } + for i in (1..=s_bias as usize).rev() { + assert!( + values[i - 1] >= values[i], + "F({}) = {} < F({}) = {} (below bias)", + i - 1, + values[i - 1], + i, + values[i] + ); + } + } + + #[test] + fn generate_lmsr_table_roundtrip_with_root() { + let values = generate_lmsr_table(1.0, 3, 10, 4, 100).unwrap(); + assert_eq!(values.len(), 8); + let root = lmsr_table_root(&values).unwrap(); + // Verify the root is deterministic + let root2 = lmsr_table_root(&values).unwrap(); + assert_eq!(root, root2); + } + + #[test] + fn generate_lmsr_table_usable_with_quote_from_table() { + use crate::lmsr_pool::math::{LmsrTradeKind, quote_from_table}; + + let q_step_lots = 10u64; + let half_payout_sats = 100u64; + let fee_bps = 30u64; + let values = generate_lmsr_table(1.0, 3, q_step_lots, 4, half_payout_sats).unwrap(); + + // Quote a buy-yes trade from index 4 → 5 + let quote = quote_from_table( + LmsrTradeKind::BuyYes, + 4, + 5, + values[4], + values[5], + q_step_lots, + half_payout_sats, + fee_bps, + ) + .unwrap(); + assert!(quote.collateral_amount > 0); + } + + #[test] + fn generate_lmsr_table_rejects_bad_depth() { + let err = generate_lmsr_table(1.0, 0, 10, 4, 100).unwrap_err(); + assert!(err.to_string().contains("table_depth must be in [1, 20]")); + let err = generate_lmsr_table(1.0, 21, 10, 4, 100).unwrap_err(); + assert!(err.to_string().contains("table_depth must be in [1, 20]")); + } + + #[test] + fn generate_lmsr_table_rejects_bad_liquidity() { + let err = generate_lmsr_table(0.0, 3, 10, 4, 100).unwrap_err(); + assert!(err.to_string().contains("liquidity_param")); + let err = generate_lmsr_table(-1.0, 3, 10, 4, 100).unwrap_err(); + assert!(err.to_string().contains("liquidity_param")); + } + + #[test] + fn generate_lmsr_table_symmetric_at_bias() { + // At the bias point, q_yes = 0, q_no = 0, so F(s_bias) is the minimum + let values = generate_lmsr_table(2.0, 4, 1, 8, 1_000).unwrap(); + // s_bias = 8 should be the minimum F value + let min_val = *values.iter().min().unwrap(); + assert_eq!(values[8], min_val); + } + + #[test] + fn generate_lmsr_table_rejects_zero_q_step_lots() { + let err = generate_lmsr_table(1.0, 3, 0, 4, 100).unwrap_err(); + assert!(err.to_string().contains("q_step_lots must be > 0")); + } + + #[test] + fn generate_lmsr_table_s_bias_beyond_table() { + // s_bias = 100 but table only has 8 entries (depth=3). + // All indices are below the bias, so all q_yes values are negative. + // This is valid — the table is just lopsided. + let values = generate_lmsr_table(1.0, 3, 10, 100, 5_000).unwrap(); + assert_eq!(values.len(), 8); + // All values should be >= the value closest to bias (last entry) + let last = values[7]; + for (i, &v) in values.iter().enumerate() { + assert!( + v >= last, + "F({i}) = {v} < F(7) = {last} — expected decreasing toward bias" + ); + } + } } diff --git a/src-tauri/crates/deadcat-sdk/src/maker_order/pset/create_order.rs b/src-tauri/crates/deadcat-sdk/src/maker_order/pset/create_order.rs index 9c845cae..26c51372 100644 --- a/src-tauri/crates/deadcat-sdk/src/maker_order/pset/create_order.rs +++ b/src-tauri/crates/deadcat-sdk/src/maker_order/pset/create_order.rs @@ -54,20 +54,35 @@ pub fn build_create_order_pset( return Err(Error::ZeroOrderAmount); } - if params.funding_utxo.value < params.order_amount { - return Err(Error::InsufficientCollateral); - } + // Detect combined mode: funding and fee share the same outpoint (same-asset case) + let combined = params.funding_utxo.outpoint == params.fee_utxo.outpoint; - if params.fee_utxo.value < params.fee_amount { - return Err(Error::InsufficientFee); - } + if combined { + // Single UTXO must cover order + fee + if params.funding_utxo.value < params.order_amount + params.fee_amount { + return Err(Error::InsufficientCollateral); + } - if params.funding_utxo.value > params.order_amount && params.change_destination.is_none() { - return Err(Error::MissingChangeDestination); - } + let total_change = params.funding_utxo.value - params.order_amount - params.fee_amount; + if total_change > 0 && params.change_destination.is_none() { + return Err(Error::MissingChangeDestination); + } + } else { + if params.funding_utxo.value < params.order_amount { + return Err(Error::InsufficientCollateral); + } + + if params.fee_utxo.value < params.fee_amount { + return Err(Error::InsufficientFee); + } + + if params.funding_utxo.value > params.order_amount && params.change_destination.is_none() { + return Err(Error::MissingChangeDestination); + } - if params.fee_utxo.value > params.fee_amount && params.fee_change_destination.is_none() { - return Err(Error::MissingChangeDestination); + if params.fee_utxo.value > params.fee_amount && params.fee_change_destination.is_none() { + return Err(Error::MissingChangeDestination); + } } let mut pset = new_pset(); @@ -80,10 +95,11 @@ pub fn build_create_order_pset( let covenant_spk = contract.script_pubkey(¶ms.maker_base_pubkey); - // Input 0: funding + // Inputs add_pset_input(&mut pset, ¶ms.funding_utxo); - // Input 1: fee - add_pset_input(&mut pset, ¶ms.fee_utxo); + if !combined { + add_pset_input(&mut pset, ¶ms.fee_utxo); + } // Output 0: order UTXO -> covenant address add_pset_output( @@ -97,27 +113,249 @@ pub fn build_create_order_pset( fee_txout(¶ms.fee_asset_id, params.fee_amount), ); - // Output 2: change from funding (optional) - let funding_change = params.funding_utxo.value - params.order_amount; - if funding_change > 0 - && let Some(ref change_spk) = params.change_destination - { - add_pset_output( - &mut pset, - explicit_txout(offered_asset, funding_change, change_spk), + if combined { + // Single combined change output (if any) + let total_change = params.funding_utxo.value - params.order_amount - params.fee_amount; + if total_change > 0 + && let Some(ref change_spk) = params.change_destination + { + add_pset_output( + &mut pset, + explicit_txout(¶ms.fee_asset_id, total_change, change_spk), + ); + } + } else { + // Output 2: change from funding (optional) + let funding_change = params.funding_utxo.value - params.order_amount; + if funding_change > 0 + && let Some(ref change_spk) = params.change_destination + { + add_pset_output( + &mut pset, + explicit_txout(offered_asset, funding_change, change_spk), + ); + } + + // Output 3: fee change (optional) + let fee_change = params.fee_utxo.value - params.fee_amount; + if fee_change > 0 + && let Some(ref change_spk) = params.fee_change_destination + { + add_pset_output( + &mut pset, + explicit_txout(¶ms.fee_asset_id, fee_change, change_spk), + ); + } + } + + Ok(pset) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::maker_order::contract::CompiledMakerOrder; + use crate::maker_order::params::{MakerOrderParams, OrderDirection}; + use crate::pset::UnblindedUtxo; + use crate::taproot::NUMS_KEY_BYTES; + use crate::testing::test_explicit_utxo; + + const BASE_ASSET: [u8; 32] = [0x01; 32]; + const QUOTE_ASSET: [u8; 32] = [0xbb; 32]; + const MAKER_PUBKEY: [u8; 32] = [0xaa; 32]; + + fn sell_quote_contract() -> CompiledMakerOrder { + let (params, _) = MakerOrderParams::new( + BASE_ASSET, + QUOTE_ASSET, + 50_000, + 1, + 1, + OrderDirection::SellQuote, + NUMS_KEY_BYTES, + &MAKER_PUBKEY, + &[0x22; 32], ); + CompiledMakerOrder::new(params).unwrap() } - // Output 3: fee change (optional) - let fee_change = params.fee_utxo.value - params.fee_amount; - if fee_change > 0 - && let Some(ref change_spk) = params.fee_change_destination - { - add_pset_output( - &mut pset, - explicit_txout(¶ms.fee_asset_id, fee_change, change_spk), + fn sell_base_contract() -> CompiledMakerOrder { + let (params, _) = MakerOrderParams::new( + BASE_ASSET, + QUOTE_ASSET, + 50_000, + 1, + 1, + OrderDirection::SellBase, + NUMS_KEY_BYTES, + &MAKER_PUBKEY, + &[0x11; 32], ); + CompiledMakerOrder::new(params).unwrap() } - Ok(pset) + /// UTXO at the default outpoint (tag=0). + fn utxo(asset_id: [u8; 32], value: u64) -> UnblindedUtxo { + test_explicit_utxo(&asset_id, value, &Script::new(), 0) + } + + /// UTXO at a distinct outpoint (tag=1). + fn utxo_separate(asset_id: [u8; 32], value: u64) -> UnblindedUtxo { + test_explicit_utxo(&asset_id, value, &Script::new(), 1) + } + + // -- Combined-UTXO detection -- + + #[test] + fn combined_detected_when_same_outpoint() { + let u = utxo(QUOTE_ASSET, 1000); + assert_eq!(u.outpoint, u.outpoint); + // Same outpoint → combined path + let contract = sell_quote_contract(); + let params = CreateOrderParams { + funding_utxo: u.clone(), + fee_utxo: u, + order_amount: 500, + fee_amount: 400, + fee_asset_id: QUOTE_ASSET, + change_destination: Some(Script::new()), + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let pset = build_create_order_pset(&contract, ¶ms).unwrap(); + assert_eq!(pset.inputs().len(), 1); + } + + #[test] + fn separate_when_different_outpoints() { + let contract = sell_base_contract(); + let params = CreateOrderParams { + funding_utxo: utxo(BASE_ASSET, 100), + fee_utxo: utxo_separate(QUOTE_ASSET, 500), + order_amount: 100, + fee_amount: 500, + fee_asset_id: QUOTE_ASSET, + change_destination: None, + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let pset = build_create_order_pset(&contract, ¶ms).unwrap(); + assert_eq!(pset.inputs().len(), 2); + } + + // -- Combined-UTXO validation -- + + #[test] + fn combined_exact_amount_no_change() { + let contract = sell_quote_contract(); + let u = utxo(QUOTE_ASSET, 600); // 500 order + 100 fee + let params = CreateOrderParams { + funding_utxo: u.clone(), + fee_utxo: u, + order_amount: 500, + fee_amount: 100, + fee_asset_id: QUOTE_ASSET, + change_destination: None, + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let pset = build_create_order_pset(&contract, ¶ms).unwrap(); + assert_eq!(pset.inputs().len(), 1); + // covenant + fee, no change + assert_eq!(pset.outputs().len(), 2); + } + + #[test] + fn combined_with_change() { + let contract = sell_quote_contract(); + let u = utxo(QUOTE_ASSET, 1000); // 500 order + 100 fee + 400 change + let params = CreateOrderParams { + funding_utxo: u.clone(), + fee_utxo: u, + order_amount: 500, + fee_amount: 100, + fee_asset_id: QUOTE_ASSET, + change_destination: Some(Script::new()), + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let pset = build_create_order_pset(&contract, ¶ms).unwrap(); + assert_eq!(pset.inputs().len(), 1); + // covenant + fee + change + assert_eq!(pset.outputs().len(), 3); + } + + #[test] + fn combined_insufficient_for_order_plus_fee() { + let contract = sell_quote_contract(); + let u = utxo(QUOTE_ASSET, 500); // enough for order, not order + fee + let params = CreateOrderParams { + funding_utxo: u.clone(), + fee_utxo: u, + order_amount: 500, + fee_amount: 100, + fee_asset_id: QUOTE_ASSET, + change_destination: None, + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let result = build_create_order_pset(&contract, ¶ms); + assert!(matches!(result, Err(Error::InsufficientCollateral))); + } + + #[test] + fn combined_excess_without_change_dest_rejected() { + let contract = sell_quote_contract(); + let u = utxo(QUOTE_ASSET, 1000); + let params = CreateOrderParams { + funding_utxo: u.clone(), + fee_utxo: u, + order_amount: 500, + fee_amount: 100, + fee_asset_id: QUOTE_ASSET, + change_destination: None, // excess but no change destination + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let result = build_create_order_pset(&contract, ¶ms); + assert!(matches!(result, Err(Error::MissingChangeDestination))); + } + + // -- Separate path still works -- + + #[test] + fn separate_insufficient_fee_rejected() { + let contract = sell_base_contract(); + let params = CreateOrderParams { + funding_utxo: utxo(BASE_ASSET, 100), + fee_utxo: utxo_separate(QUOTE_ASSET, 50), + order_amount: 100, + fee_amount: 100, + fee_asset_id: QUOTE_ASSET, + change_destination: None, + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let result = build_create_order_pset(&contract, ¶ms); + assert!(matches!(result, Err(Error::InsufficientFee))); + } + + #[test] + fn separate_with_both_changes() { + let contract = sell_base_contract(); + let params = CreateOrderParams { + funding_utxo: utxo(BASE_ASSET, 200), + fee_utxo: utxo_separate(QUOTE_ASSET, 1000), + order_amount: 100, + fee_amount: 500, + fee_asset_id: QUOTE_ASSET, + change_destination: Some(Script::new()), + fee_change_destination: Some(Script::new()), + maker_base_pubkey: MAKER_PUBKEY, + }; + let pset = build_create_order_pset(&contract, ¶ms).unwrap(); + assert_eq!(pset.inputs().len(), 2); + // covenant + fee + funding_change + fee_change + assert_eq!(pset.outputs().len(), 4); + } } diff --git a/src-tauri/crates/deadcat-sdk/src/network.rs b/src-tauri/crates/deadcat-sdk/src/network.rs index dbd127cd..a289b576 100644 --- a/src-tauri/crates/deadcat-sdk/src/network.rs +++ b/src-tauri/crates/deadcat-sdk/src/network.rs @@ -68,6 +68,20 @@ impl Network { pub fn genesis_hash(self) -> [u8; 32] { self.into_lwk().genesis_block_hash().to_byte_array() } + + /// Genesis block hash with reversed byte order, used by the swap + /// assembly path. + /// + /// The swap path does not depend on the exact genesis hash value + /// (the sighash is self-consistent regardless of byte order), so + /// this is a best-effort match against the on-chain representation. + /// For admin operations that hash `genesis_block_hash()` explicitly, + /// use [`DeadcatSdk::chain_genesis_hash`] instead. + pub fn genesis_hash_simplicity(self) -> [u8; 32] { + let mut h = self.genesis_hash(); + h.reverse(); + h + } } impl std::str::FromStr for Network { diff --git a/src-tauri/crates/deadcat-sdk/src/node.rs b/src-tauri/crates/deadcat-sdk/src/node.rs index 5e7b27cc..d6eb8850 100644 --- a/src-tauri/crates/deadcat-sdk/src/node.rs +++ b/src-tauri/crates/deadcat-sdk/src/node.rs @@ -876,6 +876,128 @@ impl DeadcatNode { Ok(snapshot) } + /// Scan a pool and return a pre-populated adjust request with current UTXOs. + /// + /// The caller sets `new_reserves`, `table_values`, `fee_amount`, and + /// `pool_index` on the returned request, then passes it to + /// [`adjust_lmsr_pool`](Self::adjust_lmsr_pool). + pub async fn scan_for_adjust( + &self, + locator: LmsrPoolLocator, + ) -> Result< + ( + LmsrPoolSnapshot, + crate::lmsr_pool::api::AdjustLmsrPoolRequest, + ), + NodeError, + > { + // Reuse the same validation as scan_lmsr_pool + if locator.hinted_s_index > locator.params.s_max_index { + return Err(NodeError::Sdk(Error::LmsrPool(format!( + "hinted_s_index {} exceeds s_max_index {}", + locator.hinted_s_index, locator.params.s_max_index + )))); + } + let creation_txid = locator.creation_txid; + let creation_txid_bytes = txid_to_canonical_bytes(&creation_txid) + .map_err(|e| NodeError::Sdk(Error::LmsrPool(e)))?; + let params = locator.params; + let expected_market_id = derive_lmsr_market_id(params); + if locator.market_id != expected_market_id { + return Err(NodeError::Sdk(Error::LmsrPool(format!( + "locator market_id {} does not match canonical market_id {}", + locator.market_id, expected_market_id + )))); + } + let expected_pool_id = derive_lmsr_pool_id( + self.network, + params, + creation_txid_bytes, + locator.initial_reserve_outpoints, + ) + .map_err(|e| NodeError::Sdk(Error::LmsrPool(e)))?; + if locator.pool_id != expected_pool_id { + return Err(NodeError::Sdk(Error::LmsrPool(format!( + "locator pool_id {} does not match canonical pool_id {}", + locator.pool_id.to_hex(), + expected_pool_id.to_hex() + )))); + } + let initial_reserve_outpoints = locator.initial_reserve_outpoints; + let hinted_s_index = locator.hinted_s_index; + let witness_schema_version = locator.witness_schema_version.clone(); + + let scan = self + .with_sdk(move |sdk| { + sdk.scan_lmsr_pool_state( + params, + creation_txid_bytes, + initial_reserve_outpoints, + hinted_s_index, + &witness_schema_version, + ) + }) + .await?; + + let current_reserve_outpoints = [ + scan.pool_utxos.yes.outpoint, + scan.pool_utxos.no.outpoint, + scan.pool_utxos.collateral.outpoint, + ]; + let last_transition_txid = if current_reserve_outpoints[0].txid == creation_txid { + None + } else { + Some(current_reserve_outpoints[0].txid) + }; + let snapshot = LmsrPoolSnapshot { + locator: locator.clone(), + current_s_index: scan.current_s_index, + reserves: scan.reserves, + current_reserve_outpoints, + last_transition_txid, + }; + self.persist_lmsr_pool_snapshot(&snapshot); + + let request = crate::lmsr_pool::api::AdjustLmsrPoolRequest { + locator, + current_pool_utxos: scan.pool_utxos, + current_s_index: scan.current_s_index, + current_reserves: scan.reserves, + // Caller must set these: + new_reserves: scan.reserves, // default: no change + table_values: Vec::new(), // caller must provide + fee_amount: 0, // caller must provide + pool_index: 0, // caller must provide + }; + + Ok((snapshot, request)) + } + + /// Adjust LMSR pool reserves via AdminAdjust transition. + pub async fn adjust_lmsr_pool( + &self, + request: crate::lmsr_pool::api::AdjustLmsrPoolRequest, + ) -> Result { + let result = self + .with_sdk(move |sdk| sdk.adjust_lmsr_pool(&request)) + .await?; + self.persist_lmsr_pool_snapshot(&result.new_snapshot); + Ok(result) + } + + /// Close an LMSR pool by adjusting reserves to covenant minimums. + /// + /// NOTE: Unlike `adjust_lmsr_pool`, this does NOT persist the post-close + /// snapshot because `CloseLmsrPoolResult` doesn't carry one. A follow-up + /// `scan_lmsr_pool` call will pick up the new on-chain state. + pub async fn close_lmsr_pool( + &self, + request: crate::lmsr_pool::api::CloseLmsrPoolRequest, + ) -> Result { + self.with_sdk(move |sdk| sdk.close_lmsr_pool(&request)) + .await + } + // ── Discovery (delegated to DiscoveryService) ─────────────────────── /// Fetch all markets from Nostr relays. @@ -942,6 +1064,24 @@ impl DeadcatNode { // ── Wallet queries (via spawn_blocking) ───────────────────────────── + /// Set the chain genesis hash for Simplicity admin operations. + /// + /// Required for regtest where each `elementsd` instance has a unique + /// genesis hash. + pub async fn set_chain_genesis_hash(&self, hash: [u8; 32]) -> Result<(), NodeError> { + self.with_sdk(move |sdk| { + sdk.set_chain_genesis_hash(hash); + Ok(()) + }) + .await + } + + /// Derive the x-only admin public key for the given pool index. + pub async fn pool_admin_pubkey(&self, pool_index: u32) -> Result<[u8; 32], NodeError> { + self.with_sdk(move |sdk| sdk.pool_admin_pubkey(pool_index)) + .await + } + /// Sync the wallet with the Electrum backend. pub async fn sync_wallet(&self) -> Result<(), NodeError> { self.with_sdk(|sdk| sdk.sync()).await diff --git a/src-tauri/crates/deadcat-sdk/src/sdk.rs b/src-tauri/crates/deadcat-sdk/src/sdk.rs index 4b7763f6..70bcae6c 100644 --- a/src-tauri/crates/deadcat-sdk/src/sdk.rs +++ b/src-tauri/crates/deadcat-sdk/src/sdk.rs @@ -19,13 +19,16 @@ use crate::assembly::{pset_to_pruning_transaction, txout_secrets_from_unblinded} use crate::chain::{ChainBackend, ElectrumBackend}; use crate::error::{Error, Result}; use crate::lmsr_pool::api::{ + AdjustLmsrPoolRequest, AdjustLmsrPoolResult, CloseLmsrPoolRequest, CloseLmsrPoolResult, CreateLmsrPoolRequest, LmsrPoolLocator, LmsrPoolSnapshot, txid_to_canonical_bytes, }; +use crate::lmsr_pool::assembly::attach_lmsr_pool_witnesses; use crate::lmsr_pool::chain_walk::{ decode_primary_witness_payload_from_spend_tx, extract_reserve_window, }; use crate::lmsr_pool::contract::CompiledLmsrPool; use crate::lmsr_pool::identity::derive_lmsr_pool_id; +use crate::lmsr_pool::math::LmsrTradeKind; use crate::lmsr_pool::params::{LmsrInitialOutpoint, LmsrPoolParams}; use crate::lmsr_pool::table::LmsrTableManifest; use crate::maker_order::contract::CompiledMakerOrder; @@ -63,6 +66,7 @@ use crate::pset::{ UnblindedUtxo, add_pset_input, add_pset_output, explicit_txout, fee_txout, new_pset, }; use crate::taproot::NUMS_KEY_BYTES; +use crate::trade::types::{LmsrPoolSwapLeg, LmsrPoolUtxos, LmsrPrimaryPath}; use crate::discovery::pool::LMSR_WITNESS_SCHEMA_V2; @@ -147,6 +151,12 @@ pub struct DeadcatSdk { wollet: Wollet, network: Network, chain: ElectrumBackend, + /// Genesis hash for the Simplicity C runtime. + /// + /// For Liquid/Testnet, this is the hardcoded constant. + /// For regtest, this must be set to the actual chain genesis hash + /// via [`set_chain_genesis_hash`](Self::set_chain_genesis_hash). + chain_genesis_override: Option<[u8; 32]>, } struct SdkPredictionMarketScanBackend<'a> { @@ -215,6 +225,7 @@ impl DeadcatSdk { wollet, network, chain: ElectrumBackend::new(electrum_url), + chain_genesis_override: None, }) } @@ -238,6 +249,25 @@ impl DeadcatSdk { Ok(()) } + /// Get the genesis block hash for Simplicity operations. + /// + /// Returns the override if set (required for regtest), otherwise + /// falls back to the hardcoded network genesis hash. + pub fn chain_genesis_hash(&self) -> Result<[u8; 32]> { + Ok(self + .chain_genesis_override + .unwrap_or_else(|| self.network.genesis_hash())) + } + + /// Set the chain genesis hash for Simplicity admin operations. + /// + /// Required for regtest where each `elementsd` instance has a unique + /// genesis hash. For Liquid and Liquid Testnet, the hardcoded values + /// are correct. + pub fn set_chain_genesis_hash(&mut self, hash: [u8; 32]) { + self.chain_genesis_override = Some(hash); + } + pub fn balance(&self) -> Result> { let balance = self .wollet @@ -478,6 +508,431 @@ impl DeadcatSdk { }) } + // ── LMSR pool admin operations ──────────────────────────────────── + + pub(crate) fn adjust_lmsr_pool( + &mut self, + request: &AdjustLmsrPoolRequest, + ) -> Result { + self.sync()?; + + // Fetch the actual chain genesis hash for admin signature computation. + // The Simplicity C runtime in elementsd uses the chain's real genesis + // hash (via `uint256::data()` in LE byte order). For regtest, each + // instance has a unique genesis hash that differs from LWK's hardcoded + // constant. + let chain_genesis = self.chain_genesis_hash()?; + + let params = request.locator.params; + if !params.has_admin_cosigner() { + return Err(Error::LmsrPool( + "adjust_lmsr_pool requires a non-NUMS admin cosigner".into(), + )); + } + params + .validate() + .map_err(|e| Error::LmsrPool(e.to_string()))?; + let admin_keypair = self.derive_pool_admin_keypair(request.pool_index)?; + let (admin_xonly, _) = admin_keypair.x_only_public_key(); + if admin_xonly.serialize() != params.cosigner_pubkey { + return Err(Error::LmsrPool( + "derived admin pubkey does not match pool cosigner_pubkey".into(), + )); + } + if request.new_reserves.r_yes < params.min_r_yes + || request.new_reserves.r_no < params.min_r_no + || request.new_reserves.r_lbtc < params.min_r_collateral + { + return Err(Error::LmsrPool("new reserves below pool minimums".into())); + } + if request.current_pool_utxos.yes.value != request.current_reserves.r_yes + || request.current_pool_utxos.no.value != request.current_reserves.r_no + || request.current_pool_utxos.collateral.value != request.current_reserves.r_lbtc + { + return Err(Error::LmsrPool( + "current_reserves must match current_pool_utxos values".into(), + )); + } + let manifest = LmsrTableManifest::new(params.table_depth, request.table_values.clone())?; + manifest.verify_matches_pool_params(¶ms)?; + + let contract = CompiledLmsrPool::new(params)?; + let s_index = request.current_s_index; + let reserve_spk = contract.script_pubkey(s_index); + + let change_addr: lwk_wollet::elements::Address = self + .address(None)? + .address() + .to_string() + .parse() + .map_err(|e| Error::Query(format!("bad change address: {e}")))?; + + let policy_asset = self.policy_asset(); + let policy_bytes = policy_asset.into_inner().to_byte_array(); + let collateral_is_policy = params.collateral_asset_id == policy_bytes; + + // Build PSET: 3 reserve inputs + optional wallet inputs → 3 reserve outputs + change + fee + let mut pset = crate::pset::new_pset(); + + // Add 3 reserve inputs (in_base = 0) + crate::pset::add_pset_input(&mut pset, &request.current_pool_utxos.yes); + crate::pset::add_pset_input(&mut pset, &request.current_pool_utxos.no); + crate::pset::add_pset_input(&mut pset, &request.current_pool_utxos.collateral); + let in_base: u32 = 0; + let out_base: u32 = 0; + + // 3 reserve outputs at same s_index + crate::pset::add_pset_output( + &mut pset, + crate::pset::explicit_txout( + ¶ms.yes_asset_id, + request.new_reserves.r_yes, + &reserve_spk, + ), + ); + crate::pset::add_pset_output( + &mut pset, + crate::pset::explicit_txout( + ¶ms.no_asset_id, + request.new_reserves.r_no, + &reserve_spk, + ), + ); + crate::pset::add_pset_output( + &mut pset, + crate::pset::explicit_txout( + ¶ms.collateral_asset_id, + request.new_reserves.r_lbtc, + &reserve_spk, + ), + ); + + // Collect any additional wallet inputs needed for adding liquidity + let mut exclude: Vec = vec![ + request.current_pool_utxos.yes.outpoint, + request.current_pool_utxos.no.outpoint, + request.current_pool_utxos.collateral.outpoint, + ]; + let mut wallet_inputs: Vec = Vec::new(); + + // YES delta + if request.new_reserves.r_yes > request.current_reserves.r_yes { + let extra = request.new_reserves.r_yes - request.current_reserves.r_yes; + let inputs = + self.collect_wallet_utxos_for_asset(¶ms.yes_asset_id, extra, &exclude)?; + exclude.extend(inputs.iter().map(|u| u.outpoint)); + for u in &inputs { + crate::pset::add_pset_input(&mut pset, u); + } + wallet_inputs.extend(inputs); + } + // NO delta + if request.new_reserves.r_no > request.current_reserves.r_no { + let extra = request.new_reserves.r_no - request.current_reserves.r_no; + let inputs = + self.collect_wallet_utxos_for_asset(¶ms.no_asset_id, extra, &exclude)?; + exclude.extend(inputs.iter().map(|u| u.outpoint)); + for u in &inputs { + crate::pset::add_pset_input(&mut pset, u); + } + wallet_inputs.extend(inputs); + } + // Collateral delta. + // + // When collateral IS the policy asset, the fee can be absorbed from the + // explicit reserve flow: + // - increase: wallet must provide (extra + fee) worth of policy asset. + // - decrease: the explicit surplus covers the fee, so we reduce the + // collateral change output by fee_amount and skip the wallet input. + // - neutral: wallet provides fee_amount. + // + // This avoids mixing confidential wallet inputs with an explicit fee + // output which would cause `value in != value out` on Elements. + let collateral_decrease = request + .current_reserves + .r_lbtc + .saturating_sub(request.new_reserves.r_lbtc); + let fee_absorbed_by_collateral_surplus = + collateral_is_policy && collateral_decrease >= request.fee_amount; + + let collateral_extra_needed = + if request.new_reserves.r_lbtc > request.current_reserves.r_lbtc { + let extra = request.new_reserves.r_lbtc - request.current_reserves.r_lbtc; + if collateral_is_policy { + extra + request.fee_amount + } else { + extra + } + } else if collateral_is_policy && !fee_absorbed_by_collateral_surplus { + // Neutral case or decrease too small to absorb fee + request.fee_amount - collateral_decrease + } else { + 0 + }; + if collateral_extra_needed > 0 { + let inputs = self.collect_wallet_utxos_for_asset( + ¶ms.collateral_asset_id, + collateral_extra_needed, + &exclude, + )?; + exclude.extend(inputs.iter().map(|u| u.outpoint)); + for u in &inputs { + crate::pset::add_pset_input(&mut pset, u); + } + wallet_inputs.extend(inputs); + } + // Fee inputs if collateral != policy asset + if !collateral_is_policy { + let fee_inputs = + self.collect_wallet_utxos_for_asset(&policy_bytes, request.fee_amount, &exclude)?; + for u in &fee_inputs { + crate::pset::add_pset_input(&mut pset, u); + } + wallet_inputs.extend(fee_inputs); + } + + // Fee output + crate::pset::add_pset_output( + &mut pset, + crate::pset::explicit_txout(&policy_bytes, request.fee_amount, &Script::new()), + ); + + // Change outputs for surplus tokens returned to wallet. + // + // All change outputs are explicit (not blinded). The reserve inputs are + // explicit on-chain, so reserve amounts are already public. Blinding the + // change would require surjection proofs referencing confidential inputs, + // but the reserve covenant inputs are explicit. + let change_spk = change_addr.script_pubkey(); + + // Reserve-surplus change (decreasing reserves → tokens flow to wallet) + if request.current_reserves.r_yes > request.new_reserves.r_yes { + let surplus = request.current_reserves.r_yes - request.new_reserves.r_yes; + crate::pset::add_pset_output( + &mut pset, + crate::pset::explicit_txout(¶ms.yes_asset_id, surplus, &change_spk), + ); + } + if request.current_reserves.r_no > request.new_reserves.r_no { + let surplus = request.current_reserves.r_no - request.new_reserves.r_no; + crate::pset::add_pset_output( + &mut pset, + crate::pset::explicit_txout(¶ms.no_asset_id, surplus, &change_spk), + ); + } + if request.current_reserves.r_lbtc > request.new_reserves.r_lbtc { + let surplus = request.current_reserves.r_lbtc - request.new_reserves.r_lbtc; + // When the fee is absorbed from this explicit surplus, reduce by fee_amount. + let net_surplus = if fee_absorbed_by_collateral_surplus { + surplus - request.fee_amount + } else { + surplus + }; + if net_surplus > 0 { + crate::pset::add_pset_output( + &mut pset, + crate::pset::explicit_txout( + ¶ms.collateral_asset_id, + net_surplus, + &change_spk, + ), + ); + } + } + + // Wallet-input surplus change (wallet provided more than needed). + // These outputs are blinded because the wallet inputs are confidential + // and Elements requires the value balance to hold with commitments. + let mut blind_indices = Vec::new(); + { + let mut wallet_totals: HashMap<[u8; 32], u64> = HashMap::new(); + for u in &wallet_inputs { + *wallet_totals.entry(u.asset_id).or_insert(0) += u.value; + } + let mut wallet_needed_per_asset: HashMap<[u8; 32], u64> = HashMap::new(); + if request.new_reserves.r_yes > request.current_reserves.r_yes { + let extra = request.new_reserves.r_yes - request.current_reserves.r_yes; + *wallet_needed_per_asset + .entry(params.yes_asset_id) + .or_insert(0) += extra; + } + if request.new_reserves.r_no > request.current_reserves.r_no { + let extra = request.new_reserves.r_no - request.current_reserves.r_no; + *wallet_needed_per_asset + .entry(params.no_asset_id) + .or_insert(0) += extra; + } + if collateral_extra_needed > 0 { + *wallet_needed_per_asset + .entry(params.collateral_asset_id) + .or_insert(0) += collateral_extra_needed; + } + if !collateral_is_policy { + *wallet_needed_per_asset.entry(policy_bytes).or_insert(0) += request.fee_amount; + } + for (asset, total) in &wallet_totals { + let needed = wallet_needed_per_asset.get(asset).copied().unwrap_or(0); + if *total > needed { + let surplus = *total - needed; + let idx = pset.n_outputs(); + crate::pset::add_pset_output( + &mut pset, + crate::pset::explicit_txout(asset, surplus, &change_spk), + ); + blind_indices.push(idx); + } + } + } + + // Blind wallet-surplus change outputs BEFORE computing the admin + // signature, since sig_all_hash covers blinded output commitments. + // + // We can't use blind_order_pset here because it indexes wallet_inputs + // from 0, but in the PSET, wallet inputs start at index 3 (after the + // 3 reserve inputs). We build the secrets map manually with correct + // PSET indices, including both reserve and wallet inputs. + if !blind_indices.is_empty() { + let blinding_pk = change_addr + .blinding_pubkey + .ok_or_else(|| Error::Blinding("change address has no blinding key".into()))?; + let pset_blinding_key = lwk_wollet::elements::bitcoin::PublicKey { + inner: blinding_pk, + compressed: true, + }; + for &idx in &blind_indices { + pset.outputs_mut()[idx].blinding_key = Some(pset_blinding_key); + pset.outputs_mut()[idx].blinder_index = Some(0); + } + + let mut inp_txout_sec = HashMap::new(); + // Reserve inputs (explicit, at PSET indices 0-2) + for (i, utxo) in [ + &request.current_pool_utxos.yes, + &request.current_pool_utxos.no, + &request.current_pool_utxos.collateral, + ] + .iter() + .enumerate() + { + let asset_id = AssetId::from_slice(&utxo.asset_id) + .map_err(|e| Error::Blinding(format!("reserve input {i} asset: {e}")))?; + inp_txout_sec.insert(i, txout_secrets_from_unblinded(utxo, asset_id)?); + } + // Wallet inputs (confidential, at PSET indices 3+) + for (i, utxo) in wallet_inputs.iter().enumerate() { + let pset_idx = 3 + i; + let asset_id = AssetId::from_slice(&utxo.asset_id) + .map_err(|e| Error::Blinding(format!("wallet input {i} asset: {e}")))?; + inp_txout_sec.insert(pset_idx, txout_secrets_from_unblinded(utxo, asset_id)?); + } + + let secp = secp256k1_zkp::Secp256k1::new(); + let mut rng = rand::thread_rng(); + pset.blind_last(&mut rng, &secp, &inp_txout_sec) + .map_err(|e| Error::Blinding(format!("{e:?}")))?; + } + + // Compute admin signature + let old_proof = manifest.proof_at(s_index)?; + let admin_signature = compute_lmsr_admin_signature( + &pset, + &contract, + ¶ms, + &request.current_pool_utxos, + s_index, + s_index, + request.current_reserves, + request.new_reserves, + in_base, + out_base, + &admin_keypair, + chain_genesis, + )?; + + // Build swap leg for witness attachment + let leg = LmsrPoolSwapLeg { + primary_path: LmsrPrimaryPath::AdminAdjust, + pool_params: params, + pool_id: request.locator.pool_id.to_hex(), + old_s_index: s_index, + new_s_index: s_index, + old_path_bits: old_proof.path_bits, + new_path_bits: old_proof.path_bits, + old_siblings: old_proof.siblings.clone(), + new_siblings: old_proof.siblings, + in_base, + out_base, + pool_utxos: request.current_pool_utxos.clone(), + trade_kind: LmsrTradeKind::BuyYes, // unused for AdminAdjust + old_f: manifest.value_at(s_index)?, + new_f: manifest.value_at(s_index)?, + delta_in: 0, + delta_out: 0, + admin_signature, + }; + + attach_lmsr_pool_witnesses(&mut pset, &leg, 0..3, chain_genesis)?; + + let tx = self.sign_pset(pset)?; + let txid = self.broadcast_and_sync(&tx)?; + + let mut new_locator = request.locator.clone(); + new_locator.hinted_s_index = s_index; + + Ok(AdjustLmsrPoolResult { + txid, + new_snapshot: LmsrPoolSnapshot { + locator: new_locator, + current_s_index: s_index, + reserves: request.new_reserves, + current_reserve_outpoints: [ + OutPoint::new(txid, 0), + OutPoint::new(txid, 1), + OutPoint::new(txid, 2), + ], + last_transition_txid: Some(txid), + }, + }) + } + + pub(crate) fn close_lmsr_pool( + &mut self, + request: &CloseLmsrPoolRequest, + ) -> Result { + let params = request.locator.params; + let min_reserves = PoolReserves { + r_yes: params.min_r_yes, + r_no: params.min_r_no, + r_lbtc: params.min_r_collateral, + }; + let adjust_request = AdjustLmsrPoolRequest { + locator: request.locator.clone(), + current_pool_utxos: request.current_pool_utxos.clone(), + current_s_index: request.current_s_index, + current_reserves: request.current_reserves, + new_reserves: min_reserves, + table_values: request.table_values.clone(), + fee_amount: request.fee_amount, + pool_index: request.pool_index, + }; + let result = self.adjust_lmsr_pool(&adjust_request)?; + Ok(CloseLmsrPoolResult { + txid: result.txid, + reclaimed_yes: request + .current_reserves + .r_yes + .saturating_sub(min_reserves.r_yes), + reclaimed_no: request + .current_reserves + .r_no + .saturating_sub(min_reserves.r_no), + reclaimed_collateral: request + .current_reserves + .r_lbtc + .saturating_sub(min_reserves.r_lbtc), + }) + } + // ── Boltz key derivation ───────────────────────────────────────────── pub fn boltz_submarine_refund_pubkey_hex(&self) -> Result { @@ -1292,6 +1747,34 @@ impl DeadcatSdk { Ok(Keypair::from_secret_key(&secp, &secret)) } + // ── Pool admin key derivation ──────────────────────────────────── + + /// Derive a secp256k1 keypair for LMSR pool admin at the given index. + /// + /// Path: `m/86'/{network}'/2'/0/{pool_index}` where `2'` = LMSR pool admin account. + fn derive_pool_admin_keypair(&self, pool_index: u32) -> Result { + let network_path = if self.network.is_mainnet() { 1776 } else { 1 }; + let path_str = format!("m/86'/{network_path}'/2'/0/{pool_index}"); + let path: lwk_wollet::bitcoin::bip32::DerivationPath = path_str + .parse() + .map_err(|e| Error::Signer(format!("{}", e)))?; + let derived = self + .signer + .derive_xprv(&path) + .map_err(|e| Error::Signer(format!("{:?}", e)))?; + let secp = secp256k1_zkp::Secp256k1::new(); + let secret = secp256k1_zkp::SecretKey::from_slice(&derived.private_key.secret_bytes()) + .map_err(|e| Error::Signer(format!("{}", e)))?; + Ok(Keypair::from_secret_key(&secp, &secret)) + } + + /// Get the x-only public key for LMSR pool admin at the given index. + pub fn pool_admin_pubkey(&self, pool_index: u32) -> Result<[u8; 32]> { + let keypair = self.derive_pool_admin_keypair(pool_index)?; + let (xonly, _parity) = keypair.x_only_public_key(); + Ok(xonly.serialize()) + } + fn collect_wallet_utxos_for_asset( &self, asset_id: &[u8; 32], @@ -1499,16 +1982,36 @@ impl DeadcatSdk { OrderDirection::SellQuote => "e_asset_id, }; - let funding_utxo = self.select_funding_utxo(offered_asset, order_amount, &[])?; + let policy_bytes: [u8; 32] = self.policy_asset().into_inner().to_byte_array(); + let same_asset = *offered_asset == policy_bytes; - // 6. Select fee UTXO (exclude funding outpoint) - let (fee_utxo, change_addr) = - self.select_fee_utxo_excluding(fee_amount, &[funding_utxo.outpoint])?; + let funding_utxo = if same_asset { + // Buy order: single UTXO covers order + fee + self.select_funding_utxo(offered_asset, order_amount + fee_amount, &[])? + } else { + self.select_funding_utxo(offered_asset, order_amount, &[])? + }; + + // 6. Select fee UTXO (or reuse funding UTXO for same-asset orders) + let (fee_utxo, change_addr) = if same_asset { + let addr_result = self.address(None)?; + let change_addr: lwk_wollet::elements::Address = addr_result + .address() + .to_string() + .parse() + .map_err(|e| Error::Query(format!("bad change address: {}", e)))?; + (funding_utxo.clone(), change_addr) + } else { + self.select_fee_utxo_excluding(fee_amount, &[funding_utxo.outpoint])? + }; let change_spk = change_addr.script_pubkey(); - let policy_bytes: [u8; 32] = self.policy_asset().into_inner().to_byte_array(); // 7. Build PSET - let input_utxos = [funding_utxo.clone(), fee_utxo.clone()]; + let input_utxos: Vec = if same_asset { + vec![funding_utxo.clone()] + } else { + vec![funding_utxo.clone(), fee_utxo.clone()] + }; let create_params = CreateOrderParams { funding_utxo, @@ -2037,6 +2540,7 @@ impl DeadcatSdk { &mut pset, lmsr_leg, lmsr_input_range, + self.network.genesis_hash_simplicity(), )?; } @@ -2575,11 +3079,6 @@ fn validate_create_lmsr_pool_request(request: &CreateLmsrPoolRequest) -> Result< .validate() .map_err(|e| Error::LmsrPool(e.to_string()))?; - if request.pool_params.cosigner_pubkey != NUMS_KEY_BYTES { - return Err(Error::LmsrPool( - "create_lmsr_pool only supports NUMS cosigner deployments in v1".into(), - )); - } if request.pool_params.yes_asset_id != request.market_params.yes_token_asset { return Err(Error::LmsrPool( "pool yes_asset_id must match market yes_token_asset".into(), @@ -2615,6 +3114,101 @@ fn validate_create_lmsr_pool_request(request: &CreateLmsrPoolRequest) -> Result< Ok(()) } +/// Compute the BIP340 admin signature for an LMSR AdminAdjust transition. +/// +/// The message hash matches the contract's `verify_admin_signature()`: +/// SHA256(domain || genesis_hash || table_root || asset_ids || sig_all_hash || +/// input_prevouts || indices || s_indices || reserve_amounts || output_spk_hashes) +#[allow(clippy::too_many_arguments)] +fn compute_lmsr_admin_signature( + pset: &PartiallySignedTransaction, + contract: &CompiledLmsrPool, + params: &LmsrPoolParams, + pool_utxos: &LmsrPoolUtxos, + old_s_index: u64, + new_s_index: u64, + current_reserves: PoolReserves, + new_reserves: PoolReserves, + in_base: u32, + out_base: u32, + admin_keypair: &Keypair, + genesis_hash: [u8; 32], +) -> Result<[u8; 64]> { + use sha2::{Digest, Sha256}; + use simplicityhl::elements::taproot::ControlBlock; + use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; + + let tx = std::sync::Arc::new(pset_to_pruning_transaction(pset)?); + let utxos: Vec = pset + .inputs() + .iter() + .enumerate() + .map(|(i, inp)| { + inp.witness_utxo + .as_ref() + .map(|u| ElementsUtxo::from(u.clone())) + .ok_or_else(|| Error::Pset(format!("input {i} missing witness_utxo"))) + }) + .collect::>>()?; + + let primary_cmr = *contract.primary_cmr(); + let cb_bytes = contract.primary_control_block(old_s_index); + let control_block = ControlBlock::from_slice(&cb_bytes) + .map_err(|e| Error::Witness(format!("admin CB: {e}")))?; + + let env = ElementsEnv::new( + tx, + utxos, + in_base, + primary_cmr, + control_block, + None, + lwk_wollet::elements::BlockHash::from_byte_array(genesis_hash), + ); + let sig_all: [u8; 32] = env.c_tx_env().sighash_all().to_byte_array(); + + // Build the admin message hash matching the contract's verify_admin_signature() + let mut hasher = Sha256::new(); + // Domain tag: "DEADCAT/LMSR_LIQUIDITY_ADJUST_V1" + hasher.update(b"DEADCAT/LMSR_LIQUIDITY_ADJUST_V1"); + hasher.update(genesis_hash); + hasher.update(params.lmsr_table_root); + hasher.update(params.yes_asset_id); + hasher.update(params.no_asset_id); + hasher.update(params.collateral_asset_id); + hasher.update(sig_all); + // Input prevouts + hasher.update(pool_utxos.yes.outpoint.txid.to_byte_array()); + hasher.update(pool_utxos.yes.outpoint.vout.to_be_bytes()); + hasher.update(pool_utxos.no.outpoint.txid.to_byte_array()); + hasher.update(pool_utxos.no.outpoint.vout.to_be_bytes()); + hasher.update(pool_utxos.collateral.outpoint.txid.to_byte_array()); + hasher.update(pool_utxos.collateral.outpoint.vout.to_be_bytes()); + // Indices and state + hasher.update(in_base.to_be_bytes()); + hasher.update(out_base.to_be_bytes()); + hasher.update(old_s_index.to_be_bytes()); + hasher.update(new_s_index.to_be_bytes()); + // Reserve amounts + hasher.update(current_reserves.r_yes.to_be_bytes()); + hasher.update(current_reserves.r_no.to_be_bytes()); + hasher.update(current_reserves.r_lbtc.to_be_bytes()); + hasher.update(new_reserves.r_yes.to_be_bytes()); + hasher.update(new_reserves.r_no.to_be_bytes()); + hasher.update(new_reserves.r_lbtc.to_be_bytes()); + // Output script pubkey hashes + let spk_hash = contract.script_hash(new_s_index); + hasher.update(spk_hash); + hasher.update(spk_hash); + hasher.update(spk_hash); + + let msg_hash: [u8; 32] = hasher.finalize().into(); + let secp = secp256k1_zkp::Secp256k1::new(); + let msg = secp256k1_zkp::Message::from_digest(msg_hash); + let sig = secp.sign_schnorr_no_aux_rand(&msg, admin_keypair); + Ok(sig.serialize()) +} + fn find_lmsr_state_index_by_script( contract: &CompiledLmsrPool, hinted_s_index: u64, diff --git a/src-tauri/crates/deadcat-sdk/tests/lmsr_pool.rs b/src-tauri/crates/deadcat-sdk/tests/lmsr_pool.rs index 38721792..34b5c9bd 100644 --- a/src-tauri/crates/deadcat-sdk/tests/lmsr_pool.rs +++ b/src-tauri/crates/deadcat-sdk/tests/lmsr_pool.rs @@ -1,6 +1,7 @@ use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; +use bitcoincore_rpc::RpcApi; use deadcat_sdk::lwk_wollet::elements::AssetId; use deadcat_sdk::lwk_wollet::elements::confidential::{Asset, Value as ConfValue}; use deadcat_sdk::taproot::NUMS_KEY_BYTES; @@ -93,6 +94,25 @@ impl Fixture { node.unlock_wallet(TEST_MNEMONIC, &env.electrum_url(), wallet_dir.path()) .expect("unlock wallet"); + // Set the actual chain genesis hash (regtest has unique genesis per instance) + { + let rpc_url = env.elements_rpc_url(); + let (user, pass) = env.elements_rpc_credentials(); + let rpc = + bitcoincore_rpc::Client::new(&rpc_url, bitcoincore_rpc::Auth::UserPass(user, pass)) + .expect("rpc client"); + let genesis_hex: String = rpc + .call("getblockhash", &[serde_json::json!(0)]) + .expect("getblockhash"); + let genesis_bytes = hex::decode(&genesis_hex).expect("decode genesis hex"); + // RPC returns display order (reversed). Convert to internal byte order. + let mut genesis: [u8; 32] = genesis_bytes.try_into().expect("32 bytes"); + genesis.reverse(); + node.set_chain_genesis_hash(genesis) + .await + .expect("set genesis hash"); + } + for _ in 0..10 { let addr = node.address(None).await.expect("wallet address"); env.elementsd_sendtoaddress(addr.address(), 200_000, None); @@ -459,3 +479,236 @@ async fn scan_lmsr_pool_bootstraps_empty_store_regtest() { .map(|outpoint| outpoint.to_string()) ); } + +/// Bootstrap a pool with a real admin cosigner (not NUMS). +/// +/// Returns the pool_index used, the create request, and the create result. +async fn bootstrap_admin_pool( + fixture: &Fixture, +) -> ( + u32, + CreateLmsrPoolRequest, + deadcat_sdk::CreateLmsrPoolResult, +) { + let pool_index: u32 = 0; + let admin_pubkey = fixture + .node + .pool_admin_pubkey(pool_index) + .await + .expect("derive admin pubkey"); + + let yes_asset_id = fixture.env.elementsd_issueasset(1_000_000); + let no_asset_id = fixture.env.elementsd_issueasset(1_000_000); + fixture.env.elementsd_generate(1); + + fixture + .fund_wallet_asset_splits(yes_asset_id, &[120_000, 110_000]) + .await; + fixture + .fund_wallet_asset_splits(no_asset_id, &[130_000, 100_000]) + .await; + + let yes_asset = asset_bytes(yes_asset_id); + let no_asset = asset_bytes(no_asset_id); + let lbtc_asset = asset_bytes(regtest_policy_asset()); + let table_values = vec![2_000, 2_010, 2_025, 2_045, 2_070, 2_100, 2_135, 2_175]; + let request = CreateLmsrPoolRequest { + market_params: PredictionMarketParams { + oracle_public_key: [0x42; 32], + collateral_asset_id: lbtc_asset, + yes_token_asset: yes_asset, + no_token_asset: no_asset, + yes_reissuance_token: [0x51; 32], + no_reissuance_token: [0x52; 32], + collateral_per_token: 100, + expiry_time: 5_000_000, + }, + pool_params: LmsrPoolParams { + yes_asset_id: yes_asset, + no_asset_id: no_asset, + collateral_asset_id: lbtc_asset, + lmsr_table_root: deadcat_sdk::lmsr_table_root(&table_values).expect("table root"), + table_depth: 3, + q_step_lots: 10, + s_bias: 4, + s_max_index: 7, + half_payout_sats: 100, + fee_bps: 30, + min_r_yes: 1, + min_r_no: 1, + min_r_collateral: 1, + cosigner_pubkey: admin_pubkey, + }, + initial_s_index: 4, + initial_reserves: PoolReserves { + r_yes: 50_000, + r_no: 50_000, + r_lbtc: 100_000, + }, + table_values: table_values.clone(), + fee_amount: 500, + }; + + let created = fixture + .node + .create_lmsr_pool(request.clone()) + .await + .expect("create admin-cosigned lmsr pool"); + mine_and_sync(&fixture.node, &fixture.env, 1).await; + (pool_index, request, created) +} + +#[tokio::test] +async fn adjust_lmsr_pool_increases_reserves_regtest() { + let _guard = hold_test_lock(); + let fixture = Fixture::new().await; + let (pool_index, request, created) = bootstrap_admin_pool(&fixture).await; + + // Scan and build an adjust request with the live UTXOs + let (snapshot, mut adjust_req) = fixture + .node + .scan_for_adjust(created.snapshot.locator.clone()) + .await + .expect("scan_for_adjust"); + + assert_eq!(snapshot.current_s_index, request.initial_s_index); + assert_eq!(snapshot.reserves, request.initial_reserves); + + // Increase all reserves by 10_000 + adjust_req.new_reserves = PoolReserves { + r_yes: snapshot.reserves.r_yes + 10_000, + r_no: snapshot.reserves.r_no + 10_000, + r_lbtc: snapshot.reserves.r_lbtc + 10_000, + }; + adjust_req.table_values = request.table_values.clone(); + adjust_req.fee_amount = 500; + adjust_req.pool_index = pool_index; + + let result = fixture + .node + .adjust_lmsr_pool(adjust_req.clone()) + .await + .expect("adjust lmsr pool"); + + // Verify the result snapshot + assert_eq!(result.new_snapshot.reserves, adjust_req.new_reserves); + assert_eq!(result.new_snapshot.current_s_index, request.initial_s_index); + assert_eq!( + result.new_snapshot.locator.pool_id, + created.snapshot.locator.pool_id + ); + assert_eq!( + result.new_snapshot.locator.market_id, + created.snapshot.locator.market_id + ); + assert_eq!( + result.new_snapshot.locator.creation_txid, + created.snapshot.locator.creation_txid + ); + + // Mine and verify on-chain + mine_and_sync(&fixture.node, &fixture.env, 1).await; + let adjust_tx = fixture + .node + .fetch_transaction(result.txid) + .await + .expect("fetch adjust tx"); + + // Reserve outputs are at indices 0, 1, 2 + let contract = deadcat_sdk::CompiledLmsrPool::new(request.pool_params).expect("compile pool"); + let reserve_script = contract.script_pubkey(request.initial_s_index); + for (index, asset_id, amount) in [ + ( + 0, + request.pool_params.yes_asset_id, + adjust_req.new_reserves.r_yes, + ), + ( + 1, + request.pool_params.no_asset_id, + adjust_req.new_reserves.r_no, + ), + ( + 2, + request.pool_params.collateral_asset_id, + adjust_req.new_reserves.r_lbtc, + ), + ] { + let output = &adjust_tx.output[index]; + assert_eq!( + output.script_pubkey, reserve_script, + "output {index} script mismatch" + ); + assert_eq!( + output.asset, + Asset::Explicit(AssetId::from_slice(&asset_id).unwrap()), + "output {index} asset mismatch" + ); + assert_eq!( + output.value, + ConfValue::Explicit(amount), + "output {index} value mismatch" + ); + } + + // Re-scan to verify the chain walk picks up the new state + let rescanned = fixture + .node + .scan_lmsr_pool(created.snapshot.locator.clone()) + .await + .expect("rescan after adjust"); + assert_eq!(rescanned.reserves, adjust_req.new_reserves); + assert_eq!(rescanned.last_transition_txid, Some(result.txid)); +} + +#[tokio::test] +async fn adjust_lmsr_pool_decreases_reserves_returns_change_regtest() { + let _guard = hold_test_lock(); + let fixture = Fixture::new().await; + let (pool_index, request, created) = bootstrap_admin_pool(&fixture).await; + + let (_, mut adjust_req) = fixture + .node + .scan_for_adjust(created.snapshot.locator.clone()) + .await + .expect("scan_for_adjust"); + + // Decrease all reserves by 10_000 (removing liquidity) + adjust_req.new_reserves = PoolReserves { + r_yes: request.initial_reserves.r_yes - 10_000, + r_no: request.initial_reserves.r_no - 10_000, + r_lbtc: request.initial_reserves.r_lbtc - 10_000, + }; + adjust_req.table_values = request.table_values.clone(); + adjust_req.fee_amount = 500; + adjust_req.pool_index = pool_index; + + let result = match fixture.node.adjust_lmsr_pool(adjust_req.clone()).await { + Ok(r) => r, + Err(e) => { + // Use RPC to get detailed rejection reason + let rpc_url = fixture.env.elements_rpc_url(); + let (user, pass) = fixture.env.elements_rpc_credentials(); + eprintln!("RPC: {rpc_url} user={user}"); + let rpc = + bitcoincore_rpc::Client::new(&rpc_url, bitcoincore_rpc::Auth::UserPass(user, pass)) + .expect("rpc client"); + // Get the genesis block hash from the node + let best = rpc + .call::("getblockhash", &[serde_json::json!(0)]) + .expect("getblockhash"); + eprintln!("Chain genesis hash: {best}"); + panic!("adjust lmsr pool (decrease): {e}"); + } + }; + assert_eq!(result.new_snapshot.reserves, adjust_req.new_reserves); + + mine_and_sync(&fixture.node, &fixture.env, 1).await; + + let rescanned = fixture + .node + .scan_lmsr_pool(created.snapshot.locator.clone()) + .await + .expect("rescan after decrease"); + assert_eq!(rescanned.reserves, adjust_req.new_reserves); +} diff --git a/src-tauri/crates/deadcat-sdk/tests/maker_order.rs b/src-tauri/crates/deadcat-sdk/tests/maker_order.rs index 7c18ad2a..bc4625cc 100644 --- a/src-tauri/crates/deadcat-sdk/tests/maker_order.rs +++ b/src-tauri/crates/deadcat-sdk/tests/maker_order.rs @@ -62,8 +62,14 @@ fn explicit_txout(asset_id: &[u8; 32], amount: u64, script_pubkey: &Script) -> T } fn test_utxo(asset_id: [u8; 32], value: u64) -> UnblindedUtxo { + test_utxo_vout(asset_id, value, 0) +} + +fn test_utxo_vout(asset_id: [u8; 32], value: u64, vout: u32) -> UnblindedUtxo { + let mut outpoint = OutPoint::default(); + outpoint.vout = vout; UnblindedUtxo { - outpoint: OutPoint::default(), + outpoint, txout: explicit_txout(&asset_id, value, &Script::new()), asset_id, value, @@ -216,7 +222,7 @@ fn create_order_happy_path() { let contract = CompiledMakerOrder::new(params).unwrap(); let create_params = CreateOrderParams { funding_utxo: test_utxo(BASE_ASSET, 100), - fee_utxo: test_utxo(FEE_ASSET, 500), + fee_utxo: test_utxo_vout(FEE_ASSET, 500, 1), order_amount: 100, fee_amount: 500, fee_asset_id: FEE_ASSET, @@ -236,7 +242,7 @@ fn create_order_with_change() { let contract = CompiledMakerOrder::new(params).unwrap(); let create_params = CreateOrderParams { funding_utxo: test_utxo(BASE_ASSET, 200), - fee_utxo: test_utxo(FEE_ASSET, 1000), + fee_utxo: test_utxo_vout(FEE_ASSET, 1000, 1), order_amount: 100, fee_amount: 500, fee_asset_id: FEE_ASSET, @@ -255,7 +261,7 @@ fn create_order_zero_amount() { let contract = CompiledMakerOrder::new(params).unwrap(); let create_params = CreateOrderParams { funding_utxo: test_utxo(BASE_ASSET, 100), - fee_utxo: test_utxo(FEE_ASSET, 500), + fee_utxo: test_utxo_vout(FEE_ASSET, 500, 1), order_amount: 0, fee_amount: 500, fee_asset_id: FEE_ASSET, @@ -273,7 +279,7 @@ fn create_order_insufficient_funding() { let contract = CompiledMakerOrder::new(params).unwrap(); let create_params = CreateOrderParams { funding_utxo: test_utxo(BASE_ASSET, 50), - fee_utxo: test_utxo(FEE_ASSET, 500), + fee_utxo: test_utxo_vout(FEE_ASSET, 500, 1), order_amount: 100, fee_amount: 500, fee_asset_id: FEE_ASSET, @@ -701,6 +707,76 @@ fn partial_fill_not_last_rejected() { )); } +// ============================================================================ +// Combined-UTXO create order tests (SellQuote, same asset for funding + fee) +// ============================================================================ + +#[test] +fn create_order_sell_quote_combined_utxo() { + let params = sell_quote_params(); + let contract = CompiledMakerOrder::new(params).unwrap(); + // Single UTXO covers order + fee exactly (same outpoint for both) + let utxo = test_utxo(QUOTE_ASSET, 500_500); // 500_000 order + 500 fee + let create_params = CreateOrderParams { + funding_utxo: utxo.clone(), + fee_utxo: utxo, + order_amount: 500_000, + fee_amount: 500, + fee_asset_id: FEE_ASSET, + change_destination: None, + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let pset = build_create_order_pset(&contract, &create_params).unwrap(); + // Combined: 1 input, 2 outputs (covenant + fee, no change) + assert_eq!(pset.inputs().len(), 1); + assert_eq!(pset.outputs().len(), 2); +} + +#[test] +fn create_order_sell_quote_combined_with_change() { + let params = sell_quote_params(); + let contract = CompiledMakerOrder::new(params).unwrap(); + let utxo = test_utxo(QUOTE_ASSET, 600_000); // excess beyond order + fee + let create_params = CreateOrderParams { + funding_utxo: utxo.clone(), + fee_utxo: utxo, + order_amount: 500_000, + fee_amount: 500, + fee_asset_id: FEE_ASSET, + change_destination: Some(Script::new()), + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let pset = build_create_order_pset(&contract, &create_params).unwrap(); + // Combined: 1 input, 3 outputs (covenant + fee + combined change) + assert_eq!(pset.inputs().len(), 1); + assert_eq!(pset.outputs().len(), 3); +} + +#[test] +fn create_order_sell_quote_combined_insufficient() { + let params = sell_quote_params(); + let contract = CompiledMakerOrder::new(params).unwrap(); + // UTXO covers order but not order + fee + let utxo = test_utxo(QUOTE_ASSET, 500_000); + let create_params = CreateOrderParams { + funding_utxo: utxo.clone(), + fee_utxo: utxo, + order_amount: 500_000, + fee_amount: 500, + fee_asset_id: FEE_ASSET, + change_destination: None, + fee_change_destination: None, + maker_base_pubkey: MAKER_PUBKEY, + }; + let result = build_create_order_pset(&contract, &create_params); + assert!(matches!( + result, + Err(deadcat_sdk::Error::InsufficientCollateral) + )); +} + // ============================================================================ // Cancel order PSET tests // ============================================================================ @@ -709,7 +785,7 @@ fn partial_fill_not_last_rejected() { fn cancel_order_happy_path() { let cancel_params = CancelOrderParams { order_utxo: test_utxo(BASE_ASSET, 100), - fee_utxo: test_utxo(FEE_ASSET, 500), + fee_utxo: test_utxo_vout(FEE_ASSET, 500, 1), fee_amount: 500, fee_asset_id: FEE_ASSET, order_asset_id: BASE_ASSET, @@ -726,7 +802,7 @@ fn cancel_order_happy_path() { fn cancel_order_with_fee_change() { let cancel_params = CancelOrderParams { order_utxo: test_utxo(BASE_ASSET, 100), - fee_utxo: test_utxo(FEE_ASSET, 1000), + fee_utxo: test_utxo_vout(FEE_ASSET, 1000, 1), fee_amount: 500, fee_asset_id: FEE_ASSET, order_asset_id: BASE_ASSET, @@ -742,7 +818,7 @@ fn cancel_order_with_fee_change() { fn cancel_order_insufficient_fee() { let cancel_params = CancelOrderParams { order_utxo: test_utxo(BASE_ASSET, 100), - fee_utxo: test_utxo(FEE_ASSET, 100), + fee_utxo: test_utxo_vout(FEE_ASSET, 100, 1), fee_amount: 500, fee_asset_id: FEE_ASSET, order_asset_id: BASE_ASSET, @@ -784,7 +860,7 @@ fn create_order_sell_quote() { let contract = CompiledMakerOrder::new(params).unwrap(); let create_params = CreateOrderParams { funding_utxo: test_utxo(QUOTE_ASSET, 500_000), - fee_utxo: test_utxo(FEE_ASSET, 500), + fee_utxo: test_utxo_vout(FEE_ASSET, 500, 1), order_amount: 500_000, fee_amount: 500, fee_asset_id: FEE_ASSET, @@ -879,7 +955,7 @@ fn create_order_zero_price_rejected() { let contract = CompiledMakerOrder::new(params).unwrap(); let create_params = CreateOrderParams { funding_utxo: test_utxo(BASE_ASSET, 100), - fee_utxo: test_utxo(FEE_ASSET, 500), + fee_utxo: test_utxo_vout(FEE_ASSET, 500, 1), order_amount: 100, fee_amount: 500, fee_asset_id: FEE_ASSET, @@ -947,7 +1023,7 @@ fn create_order_excess_funding_no_change_rejected() { let contract = CompiledMakerOrder::new(params).unwrap(); let create_params = CreateOrderParams { funding_utxo: test_utxo(BASE_ASSET, 200), - fee_utxo: test_utxo(FEE_ASSET, 500), + fee_utxo: test_utxo_vout(FEE_ASSET, 500, 1), order_amount: 100, fee_amount: 500, fee_asset_id: FEE_ASSET, diff --git a/src-tauri/src/chain_adapter.rs b/src-tauri/src/chain_adapter.rs index 87c59b1f..eb70bc65 100644 --- a/src-tauri/src/chain_adapter.rs +++ b/src-tauri/src/chain_adapter.rs @@ -52,25 +52,27 @@ impl ElectrumChainAdapter { txid: &[u8; 32], ) -> Result, ChainAdapterError> { let client = self.client()?; - let txid_hex = txid_to_display_hex(txid); - let resp = match transaction_get_response(&client, &txid_hex, true)? { - Some(resp) => resp, + + let height = match get_tx_confirmed_height(&client, self, txid)? { + Some(h) => h, None => return Ok(None), }; - let confirmations = resp["confirmations"].as_u64().unwrap_or(0) as u32; + let confirmations = best_height + .checked_sub(height) + .map(|diff| diff + 1) + .unwrap_or(0); + if confirmations < deadcat_store::LIQUID_IRREVERSIBLE_CONFIRMATIONS { return Ok(None); } - let block_hash_hex = resp["blockhash"] - .as_str() - .ok_or_else(|| ChainAdapterError::Parse("missing blockhash".into()))?; - let block_hash = hex_to_txid_bytes(block_hash_hex)?; - confirmation_height_at(best_height, confirmations, block_hash) + let block_hash = get_block_hash(&client, height)?; + Ok(Some((height, block_hash))) } } +#[cfg(test)] fn confirmation_height_at( best_height: u32, confirmations: u32, @@ -86,6 +88,90 @@ fn confirmation_height_at( Ok(Some((confirmed_height, block_hash))) } +/// Get the confirmed height of a transaction by looking up its first output's +/// script hash history. Returns `None` if the tx is unconfirmed or not found. +fn get_tx_confirmed_height( + client: &electrum_client::Client, + adapter: &ElectrumChainAdapter, + txid: &[u8; 32], +) -> Result, ChainAdapterError> { + use electrum_client::ElectrumApi; + + let txid_hex = txid_to_display_hex(txid); + + // Fetch raw tx (non-verbose, which is supported) + let raw_tx = match adapter.get_transaction(txid)? { + Some(tx) => tx, + None => return Ok(None), + }; + + // Deserialize to get the first output's script_pubkey + let tx: lwk_wollet::elements::Transaction = lwk_wollet::elements::encode::deserialize(&raw_tx) + .map_err(|e| ChainAdapterError::Parse(format!("tx deserialize: {e}")))?; + + let first_output = match tx.output.first() { + Some(o) => o, + None => return Ok(None), + }; + + let script_hash_hex = + ElectrumChainAdapter::script_hash_hex(first_output.script_pubkey.as_bytes()); + + // Get script history and find our txid + let history = client + .raw_call( + "blockchain.scripthash.get_history", + [electrum_client::Param::String(script_hash_hex)], + ) + .map_err(|e| ChainAdapterError::Electrum(e.to_string()))?; + + if let Some(entries) = history.as_array() { + for entry in entries { + if entry["tx_hash"].as_str() == Some(&txid_hex) { + let height = entry["height"].as_u64().unwrap_or(0) as u32; + if height == 0 { + return Ok(None); // unconfirmed + } + return Ok(Some(height)); + } + } + } + + Ok(None) // txid not found in history +} + +/// Get the block hash at a given height by fetching the raw header and +/// computing its double-SHA256 hash (Elements standard). +fn get_block_hash( + client: &electrum_client::Client, + height: u32, +) -> Result<[u8; 32], ChainAdapterError> { + use electrum_client::ElectrumApi; + + let resp = client + .raw_call( + "blockchain.block.header", + [electrum_client::Param::Usize(height as usize)], + ) + .map_err(|e| ChainAdapterError::Electrum(e.to_string()))?; + + let header_hex = resp + .as_str() + .ok_or_else(|| ChainAdapterError::Parse("expected string for block header".into()))?; + + let header_bytes = hex::decode(header_hex) + .map_err(|e| ChainAdapterError::Parse(format!("block header hex decode: {e}")))?; + + // Double-SHA256 + let first = Sha256::digest(&header_bytes); + let second = Sha256::digest(first); + + let mut hash = [0u8; 32]; + hash.copy_from_slice(&second); + hash.reverse(); // internal byte order + Ok(hash) +} + fn transaction_get_response( client: &electrum_client::Client, txid_hex: &str, @@ -455,6 +541,30 @@ mod tests { assert_eq!(err.to_string(), "parse error: invalid confirmation height"); } + #[test] + fn get_block_hash_double_sha256_and_reverse() { + // Verify the double-SHA256 + byte reversal logic in isolation. + // Use a synthetic 80-byte "header" so the test is self-contained. + let header = [0xABu8; 80]; + let first = Sha256::digest(header); + let second = Sha256::digest(first); + + let mut expected = [0u8; 32]; + expected.copy_from_slice(&second); + expected.reverse(); + + // Simulate what get_block_hash does after fetching the hex + let header_hex = hex::encode(header); + let decoded = hex::decode(&header_hex).unwrap(); + let h1 = Sha256::digest(&decoded); + let h2 = Sha256::digest(h1); + let mut result = [0u8; 32]; + result.copy_from_slice(&h2); + result.reverse(); + + assert_eq!(result, expected); + } + #[test] fn transaction_get_not_found_classifier_accepts_protocol_not_found() { let err = electrum_client::Error::Protocol(serde_json::json!({ diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index f7f3921e..af239f9d 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -7,7 +7,8 @@ use serde::{Deserialize, Serialize}; use tauri::{Emitter, Manager}; use crate::discovery::{ - self, ContractMetadata, CreateContractRequest, DiscoveredMarket, IdentityResponse, + self, ContractMetadata, CreateContractRequest, DiscoveredMarket, DiscoveredOrder, + IdentityResponse, }; use crate::state::AppStateManager; use crate::{NodeState, NostrAppState}; @@ -657,6 +658,23 @@ pub async fn discover_contracts(app: tauri::AppHandle) -> Result, + app: tauri::AppHandle, +) -> Result, String> { + let node_state = app.state::(); + let guard = node_state.node.lock().await; + let node = guard.as_ref().ok_or("Node not initialized")?; + match node.fetch_orders(market_id.as_deref()).await { + Ok(orders) => Ok(orders), + Err(e) => { + log::warn!("Nostr order fetch failed: {e}"); + Ok(vec![]) + } + } +} + /// Publish a contract to Nostr (Nostr-only mode — no on-chain tx). #[tauri::command] pub async fn publish_contract( @@ -1621,3 +1639,632 @@ fn parse_iso_datetime_to_unix(s: &str) -> u64 { .map(|dt| dt.and_utc().timestamp() as u64) .unwrap_or(0) } + +// ========================================================================= +// Limit order commands +// ========================================================================= + +#[derive(Serialize, Deserialize)] +pub struct CreateLimitOrderRequest { + pub contract_params_json: String, + pub market_id: String, + pub side: String, + pub direction: String, + pub price: u64, + pub amount: u64, + #[serde(default)] + pub fee_amount: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct CreateLimitOrderResponse { + pub txid: String, + pub nostr_event_id: String, + pub covenant_address: String, + pub order_amount: u64, + pub order_index: u32, +} + +#[tauri::command] +pub async fn create_limit_order( + request: CreateLimitOrderRequest, + app: tauri::AppHandle, +) -> Result { + let params: deadcat_sdk::PredictionMarketParams = + serde_json::from_str(&request.contract_params_json) + .map_err(|e| format!("invalid contract params: {e}"))?; + let side = parse_trade_side(&request.side)?; + let direction = parse_trade_direction(&request.direction)?; + + let base_asset_id = match side { + deadcat_sdk::TradeSide::Yes => params.yes_token_asset, + deadcat_sdk::TradeSide::No => params.no_token_asset, + }; + let quote_asset_id = params.collateral_asset_id; + + let order_direction = match direction { + deadcat_sdk::TradeDirection::Buy => deadcat_sdk::OrderDirection::SellQuote, + deadcat_sdk::TradeDirection::Sell => deadcat_sdk::OrderDirection::SellBase, + }; + + let direction_label = format!( + "{}-{}", + match direction { + deadcat_sdk::TradeDirection::Buy => "buy", + deadcat_sdk::TradeDirection::Sell => "sell", + }, + match side { + deadcat_sdk::TradeSide::Yes => "yes", + deadcat_sdk::TradeSide::No => "no", + } + ); + + let order_index: u32 = 0; + + let fee_amount = request.fee_amount.unwrap_or(500); + + let node_state = app.state::(); + let guard = node_state.node.lock().await; + let node = guard.as_ref().ok_or("Node not initialized")?; + let market_id_for_store = request.market_id.clone(); + let direction_label_for_store = direction_label.clone(); + + let (result, event_id) = node + .create_limit_order( + base_asset_id, + quote_asset_id, + request.price, + request.amount, + order_direction, + 1, + 1, + order_index, + fee_amount, + request.market_id, + direction_label, + ) + .await + .map_err(|e| format!("{e}"))?; + drop(guard); + + // Persist the order to the local store for transaction labeling + { + let store_arc = { + let state_handle = app.state::>(); + let mgr = state_handle.lock().ok(); + mgr.and_then(|m| m.store().cloned()) + }; + if let Some(store_arc) = store_arc { + if let Ok(mut store) = store_arc.lock() { + // Ingest the order (deduplicates on cmr + maker_base_pubkey) + let event_id_hex = event_id.to_hex(); + if let Err(e) = store.ingest_maker_order( + &result.order_params, + Some(&result.maker_base_pubkey), + Some(&result.order_nonce), + Some(event_id_hex.as_str()), + None, + ) { + log::warn!("failed to ingest order into store: {e}"); + } + // Record the creation metadata + let compiled = deadcat_sdk::CompiledMakerOrder::new(result.order_params); + if let Ok(compiled) = compiled { + if let Err(e) = store.record_order_creation( + compiled.cmr().as_ref(), + &result.maker_base_pubkey, + &result.txid.to_string(), + &market_id_for_store, + &direction_label_for_store, + result.order_amount, + ) { + log::warn!("failed to record order creation: {e}"); + } + } + } + } + } + + bump_revision_and_emit(&app).await?; + + Ok(CreateLimitOrderResponse { + txid: result.txid.to_string(), + nostr_event_id: event_id.to_hex(), + covenant_address: result.covenant_address, + order_amount: result.order_amount, + order_index, + }) +} + +#[derive(Serialize, Deserialize)] +pub struct CancelLimitOrderRequest { + pub market_id: String, + pub base_asset_id: String, + pub quote_asset_id: String, + pub price: u64, + pub min_fill_lots: u64, + pub min_remainder_lots: u64, + pub direction: String, + pub maker_base_pubkey: String, + pub order_nonce: String, + pub cosigner_pubkey: String, + pub maker_receive_spk_hash: String, + #[serde(default)] + pub fee_amount: Option, + #[serde(default)] + pub order_index: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct CancelLimitOrderResponse { + pub txid: String, + pub refunded_amount: u64, +} + +fn decode_hex_32(hex_str: &str, field: &str) -> Result<[u8; 32], String> { + hex::decode(hex_str) + .map_err(|e| format!("invalid {field} hex: {e}"))? + .try_into() + .map_err(|_| format!("{field} must be exactly 32 bytes")) +} + +fn parse_order_direction(direction: &str) -> Result { + match direction.trim().to_ascii_lowercase().as_str() { + "sell-base" => Ok(deadcat_sdk::OrderDirection::SellBase), + "sell-quote" => Ok(deadcat_sdk::OrderDirection::SellQuote), + other => Err(format!( + "invalid order direction '{other}', expected 'sell-base' or 'sell-quote'" + )), + } +} + +#[tauri::command] +pub async fn cancel_limit_order( + request: CancelLimitOrderRequest, + app: tauri::AppHandle, +) -> Result { + let base_asset_id = decode_hex_32(&request.base_asset_id, "base_asset_id")?; + let quote_asset_id = decode_hex_32(&request.quote_asset_id, "quote_asset_id")?; + let maker_pubkey = decode_hex_32(&request.maker_base_pubkey, "maker_base_pubkey")?; + let cosigner_pubkey = decode_hex_32(&request.cosigner_pubkey, "cosigner_pubkey")?; + let maker_receive_spk_hash = + decode_hex_32(&request.maker_receive_spk_hash, "maker_receive_spk_hash")?; + let direction = parse_order_direction(&request.direction)?; + + let params = deadcat_sdk::MakerOrderParams { + base_asset_id, + quote_asset_id, + price: request.price, + min_fill_lots: request.min_fill_lots, + min_remainder_lots: request.min_remainder_lots, + direction, + maker_receive_spk_hash, + cosigner_pubkey, + maker_pubkey, + }; + + let fee_amount = request.fee_amount.unwrap_or(500); + + let order_index: u32 = request.order_index.unwrap_or(0); + + let node_state = app.state::(); + let guard = node_state.node.lock().await; + let node = guard.as_ref().ok_or("Node not initialized")?; + let result = node + .cancel_limit_order(params, maker_pubkey, order_index, fee_amount) + .await + .map_err(|e| format!("{e}"))?; + drop(guard); + + bump_revision_and_emit(&app).await?; + + Ok(CancelLimitOrderResponse { + txid: result.txid.to_string(), + refunded_amount: result.refunded_amount, + }) +} + +// ========================================================================= +// Own order listing (for transaction labeling) +// ========================================================================= + +#[derive(Serialize)] +pub struct OwnOrderSummary { + pub creation_txid: Option, + pub market_id: Option, + pub direction_label: Option, + pub price: u64, + pub offered_amount: Option, + pub order_status: String, +} + +#[tauri::command] +pub fn list_own_orders(app: tauri::AppHandle) -> Result, String> { + let store_arc = { + let state_handle = app.state::>(); + let mgr = state_handle + .lock() + .map_err(|_| "state lock failed".to_string())?; + mgr.store() + .cloned() + .ok_or_else(|| "Store not initialized".to_string())? + }; + + let mut store = store_arc + .lock() + .map_err(|_| "store lock failed".to_string())?; + + // Only return orders that have local creation metadata (creation_txid IS NOT NULL) + let all_orders = store + .list_maker_orders(&deadcat_store::OrderFilter::default()) + .map_err(|e| format!("list orders: {e}"))?; + + let own: Vec = all_orders + .into_iter() + .filter(|o| o.creation_txid.is_some()) + .map(|o| OwnOrderSummary { + creation_txid: o.creation_txid, + market_id: o.market_id, + direction_label: o.direction_label, + price: o.params.price, + offered_amount: o.offered_amount, + order_status: format!("{:?}", o.status), + }) + .collect(); + + Ok(own) +} + +// ========================================================================= +// LMSR Pool commands +// ========================================================================= + +#[tauri::command] +pub fn generate_lmsr_table( + liquidity_param: f64, + table_depth: u32, + q_step_lots: u64, + s_bias: u64, + half_payout_sats: u64, +) -> Result, String> { + deadcat_sdk::generate_lmsr_table( + liquidity_param, + table_depth, + q_step_lots, + s_bias, + half_payout_sats, + ) + .map_err(|e| format!("{e}")) +} + +#[derive(Deserialize)] +pub struct CreateLmsrPoolRequest { + pub market_params_json: String, + pub pool_params_json: String, + pub initial_s_index: u64, + pub initial_reserves_yes: u64, + pub initial_reserves_no: u64, + pub initial_reserves_lbtc: u64, + pub table_values: Vec, + pub fee_amount: Option, +} + +#[derive(Serialize)] +pub struct CreateLmsrPoolResponse { + pub txid: String, + pub pool_id: String, +} + +#[tauri::command] +pub async fn create_lmsr_pool( + request: CreateLmsrPoolRequest, + app: tauri::AppHandle, +) -> Result { + let market_params: deadcat_sdk::PredictionMarketParams = + serde_json::from_str(&request.market_params_json) + .map_err(|e| format!("invalid market params: {e}"))?; + let pool_params: deadcat_sdk::LmsrPoolParams = serde_json::from_str(&request.pool_params_json) + .map_err(|e| format!("invalid pool params: {e}"))?; + + let sdk_request = deadcat_sdk::CreateLmsrPoolRequest { + market_params, + pool_params, + initial_s_index: request.initial_s_index, + initial_reserves: deadcat_sdk::PoolReserves { + r_yes: request.initial_reserves_yes, + r_no: request.initial_reserves_no, + r_lbtc: request.initial_reserves_lbtc, + }, + table_values: request.table_values, + fee_amount: request.fee_amount.unwrap_or(500), + }; + + let node_state = app.state::(); + let guard = node_state.node.lock().await; + let node = guard.as_ref().ok_or("Node not initialized")?; + let result = node + .create_lmsr_pool(sdk_request) + .await + .map_err(|e| format!("{e}"))?; + drop(guard); + + bump_revision_and_emit(&app).await?; + + Ok(CreateLmsrPoolResponse { + txid: result.txid.to_string(), + pool_id: result.snapshot.locator.pool_id.to_hex(), + }) +} + +#[derive(Serialize)] +pub struct ScanLmsrPoolResponse { + pub pool_id: String, + pub current_s_index: u64, + pub reserve_yes: u64, + pub reserve_no: u64, + pub reserve_collateral: u64, +} + +#[tauri::command] +pub async fn scan_lmsr_pool( + pool_id: String, + app: tauri::AppHandle, +) -> Result { + // Look up the pool locator from the store + let store_arc = { + let state_handle = app.state::>(); + let mgr = state_handle + .lock() + .map_err(|_| "state lock failed".to_string())?; + mgr.store() + .cloned() + .ok_or_else(|| "Store not initialized".to_string())? + }; + + let pool_info = { + let mut store = store_arc + .lock() + .map_err(|_| "store lock failed".to_string())?; + let pools = store + .list_lmsr_pools(&deadcat_store::LmsrPoolFilter { + pool_id: Some(pool_id.clone()), + ..Default::default() + }) + .map_err(|e| format!("list pools: {e}"))?; + pools.into_iter().next().ok_or("Pool not found in store")? + }; + + let pool_params: deadcat_sdk::LmsrPoolParams = serde_json::from_str(&pool_info.params_json) + .map_err(|e| format!("invalid pool params: {e}"))?; + + let locator = deadcat_sdk::LmsrPoolLocator { + market_id: deadcat_sdk::MarketId( + hex::decode(&pool_info.market_id) + .map_err(|e| format!("bad market_id hex: {e}"))? + .try_into() + .map_err(|_| "market_id not 32 bytes")?, + ), + pool_id: deadcat_sdk::LmsrPoolId::from_hex(&pool_id).map_err(|e| format!("{e}"))?, + params: pool_params, + creation_txid: pool_info + .creation_txid + .parse() + .map_err(|e| format!("bad txid: {e}"))?, + initial_reserve_outpoints: { + let creation_txid_bytes: [u8; 32] = hex::decode(&pool_info.creation_txid) + .map_err(|e| format!("bad creation_txid hex: {e}"))? + .try_into() + .map_err(|_| "creation_txid not 32 bytes".to_string())?; + [ + deadcat_sdk::LmsrInitialOutpoint { + txid: creation_txid_bytes, + vout: 0, + }, + deadcat_sdk::LmsrInitialOutpoint { + txid: creation_txid_bytes, + vout: 1, + }, + deadcat_sdk::LmsrInitialOutpoint { + txid: creation_txid_bytes, + vout: 2, + }, + ] + }, + hinted_s_index: pool_info.current_s_index, + witness_schema_version: pool_info.witness_schema_version, + }; + + let node_state = app.state::(); + let guard = node_state.node.lock().await; + let node = guard.as_ref().ok_or("Node not initialized")?; + let snapshot = node + .scan_lmsr_pool(locator) + .await + .map_err(|e| format!("{e}"))?; + drop(guard); + + Ok(ScanLmsrPoolResponse { + pool_id, + current_s_index: snapshot.current_s_index, + reserve_yes: snapshot.reserves.r_yes, + reserve_no: snapshot.reserves.r_no, + reserve_collateral: snapshot.reserves.r_lbtc, + }) +} + +#[derive(Deserialize)] +pub struct AdjustLmsrPoolTauriRequest { + pub pool_id: String, + pub new_reserves_yes: u64, + pub new_reserves_no: u64, + pub new_reserves_lbtc: u64, + pub table_values: Vec, + pub fee_amount: Option, + pub pool_index: Option, +} + +#[derive(Serialize)] +pub struct AdjustLmsrPoolResponse { + pub txid: String, + pub pool_id: String, + pub current_s_index: u64, + pub reserve_yes: u64, + pub reserve_no: u64, + pub reserve_collateral: u64, +} + +/// Adjust an LMSR pool's reserves via AdminAdjust transition. +/// +/// Requires the pool scan to return unblinded UTXOs — not yet wired. +/// Returns an error until full pool-UTXO passthrough is implemented. +#[tauri::command] +pub async fn adjust_lmsr_pool( + _request: AdjustLmsrPoolTauriRequest, + _app: tauri::AppHandle, +) -> Result { + Err("adjust_lmsr_pool is not yet fully wired — pool UTXO passthrough required".to_string()) +} + +#[derive(Deserialize)] +pub struct CloseLmsrPoolTauriRequest { + pub pool_id: String, + pub table_values: Vec, + pub fee_amount: Option, + pub pool_index: Option, +} + +#[derive(Serialize)] +pub struct CloseLmsrPoolResponse { + pub txid: String, + pub reclaimed_yes: u64, + pub reclaimed_no: u64, + pub reclaimed_collateral: u64, +} + +/// Close an LMSR pool by adjusting reserves to covenant minimums. +/// +/// Requires the pool scan to return unblinded UTXOs — not yet wired. +/// Returns an error until full pool-UTXO passthrough is implemented. +#[tauri::command] +pub async fn close_lmsr_pool( + _request: CloseLmsrPoolTauriRequest, + _app: tauri::AppHandle, +) -> Result { + Err("close_lmsr_pool is not yet fully wired — pool UTXO passthrough required".to_string()) +} + +#[derive(Serialize)] +pub struct LmsrPoolInfoResponse { + pub pool_id: String, + pub market_id: String, + pub creation_txid: String, + pub current_s_index: u64, + pub reserve_yes: u64, + pub reserve_no: u64, + pub reserve_collateral: u64, + pub state_source: String, + pub params_json: String, + pub created_at: String, + pub updated_at: String, +} + +#[tauri::command] +pub fn list_lmsr_pools( + market_id: Option, + app: tauri::AppHandle, +) -> Result, String> { + let store_arc = { + let state_handle = app.state::>(); + let mgr = state_handle + .lock() + .map_err(|_| "state lock failed".to_string())?; + mgr.store() + .cloned() + .ok_or_else(|| "Store not initialized".to_string())? + }; + + let mut store = store_arc + .lock() + .map_err(|_| "store lock failed".to_string())?; + + let pools = store + .list_lmsr_pools(&deadcat_store::LmsrPoolFilter { + market_id, + ..Default::default() + }) + .map_err(|e| format!("list pools: {e}"))?; + + Ok(pools + .into_iter() + .map(|p| LmsrPoolInfoResponse { + pool_id: p.pool_id, + market_id: p.market_id, + creation_txid: p.creation_txid, + current_s_index: p.current_s_index, + reserve_yes: p.reserve_yes, + reserve_no: p.reserve_no, + reserve_collateral: p.reserve_collateral, + state_source: p.state_source, + params_json: p.params_json, + created_at: p.created_at, + updated_at: p.updated_at, + }) + .collect()) +} + +#[derive(Serialize)] +pub struct PriceHistoryEntryResponse { + pub pool_id: String, + pub market_id: String, + pub transition_txid: String, + pub old_s_index: u64, + pub new_s_index: u64, + pub reserve_yes: u64, + pub reserve_no: u64, + pub reserve_collateral: u64, + pub implied_yes_price_bps: u16, + pub recorded_at: String, + pub block_height: Option, +} + +#[tauri::command] +pub fn get_price_history( + market_id: String, + limit: Option, + app: tauri::AppHandle, +) -> Result, String> { + let store_arc = { + let state_handle = app.state::>(); + let mgr = state_handle + .lock() + .map_err(|_| "state lock failed".to_string())?; + mgr.store() + .cloned() + .ok_or_else(|| "Store not initialized".to_string())? + }; + + let mut store = store_arc + .lock() + .map_err(|_| "store lock failed".to_string())?; + + let entries = store + .get_market_price_history(&market_id, None, limit) + .map_err(|e| format!("get price history: {e}"))?; + + Ok(entries + .into_iter() + .map(|e| PriceHistoryEntryResponse { + pool_id: e.pool_id, + market_id: e.market_id, + transition_txid: e.transition_txid, + old_s_index: e.old_s_index, + new_s_index: e.new_s_index, + reserve_yes: e.reserve_yes, + reserve_no: e.reserve_no, + reserve_collateral: e.reserve_collateral, + implied_yes_price_bps: e.implied_yes_price_bps, + recorded_at: e.recorded_at, + block_height: e.block_height, + }) + .collect()) +} diff --git a/src-tauri/src/discovery.rs b/src-tauri/src/discovery.rs index d35da749..27515960 100644 --- a/src-tauri/src/discovery.rs +++ b/src-tauri/src/discovery.rs @@ -34,7 +34,7 @@ pub use deadcat_sdk::{ DEFAULT_RELAYS, NETWORK_TAG, }; -pub use deadcat_sdk::{ContractAnnouncement, ContractMetadata}; +pub use deadcat_sdk::{ContractAnnouncement, ContractMetadata, DiscoveredOrder}; // --------------------------------------------------------------------------- // App-layer-only types (Tauri command request/response types) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d4ecfb83..67f736ee 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1245,6 +1245,18 @@ pub fn run() { commands::execute_trade, commands::get_wallet_utxos, commands::list_contracts, + commands::fetch_orders, + commands::create_limit_order, + commands::cancel_limit_order, + commands::list_own_orders, + // LMSR Pools + commands::generate_lmsr_table, + commands::create_lmsr_pool, + commands::scan_lmsr_pool, + commands::adjust_lmsr_pool, + commands::close_lmsr_pool, + commands::list_lmsr_pools, + commands::get_price_history, // Wallet store (SDK) wallet_store::create_software_signer, wallet_store::create_wollet, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b4b2eb60..f311084c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Deadcat.Live", "version": "0.1.0", - "identifier": "com.tauri.dev", + "identifier": "io.resolvr.deadcat", "build": { "frontendDist": "../dist", "devUrl": "http://localhost:1420", diff --git a/src/actions.ts b/src/actions.ts index fe5ad5ab..4edff780 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -141,6 +141,8 @@ export const MARKET_ACTION_LIST = [ "submit-redeem", "submit-cancel", "submit-create-market", + "toggle-pool-create", + "create-pool", ] as const; export type OnboardingAction = (typeof ONBOARDING_ACTION_LIST)[number]; diff --git a/src/components/chart-series.ts b/src/components/chart-series.ts index fcbc15f3..c529412c 100644 --- a/src/components/chart-series.ts +++ b/src/components/chart-series.ts @@ -1,5 +1,5 @@ import { state } from "../state.ts"; -import type { Market } from "../types.ts"; +import type { Market, PriceHistoryEntry } from "../types.ts"; export type ChartSeriesData = { endTime: Date; @@ -116,3 +116,112 @@ export function buildChartSeriesData(market: Market): ChartSeriesData { yesSeries, }; } + +export function buildChartFromHistory( + market: Market, + history: PriceHistoryEntry[], +): ChartSeriesData { + const scaleHoursByKey: Record<"1H" | "3H" | "6H" | "12H" | "1D", number> = { + "1H": 1, + "3H": 3, + "6H": 6, + "12H": 12, + "1D": 24, + }; + const pointCountByKey: Record<"1H" | "3H" | "6H" | "12H" | "1D", number> = { + "1H": 28, + "3H": 34, + "6H": 40, + "12H": 48, + "1D": 56, + }; + const scaleKey = state.chartTimescale; + const scaleHours = scaleHoursByKey[scaleKey]; + const pointCount = pointCountByKey[scaleKey]; + const endTime = new Date(); + const startTime = new Date(endTime.getTime() - scaleHours * 60 * 60 * 1000); + const xLabelFractions = + scaleHours >= 12 ? [0, 0.25, 0.5, 0.75, 1] : [0, 1 / 3, 2 / 3, 1]; + const xLabels = xLabelFractions.map( + (fraction) => + new Date( + startTime.getTime() + + fraction * (endTime.getTime() - startTime.getTime()), + ), + ); + const xLabelOffsets = xLabelFractions.map( + (fraction) => scaleHours * (1 - fraction), + ); + + // Convert history entries to timestamped probability values + const historyPoints = history + .map((entry) => ({ + time: new Date(entry.recorded_at).getTime(), + probability: Math.max( + 0.02, + Math.min(0.98, entry.implied_yes_price_bps / 10000), + ), + })) + .filter((p) => p.time >= startTime.getTime() && p.time <= endTime.getTime()) + .sort((a, b) => a.time - b.time); + + // Resample to standard pointCount + const timeSpan = endTime.getTime() - startTime.getTime(); + const yesSeries: number[] = []; + const fallbackYes = market.yesPrice ?? 0.5; + + if (historyPoints.length === 0) { + // No history in window — flat line at current price + for (let i = 0; i < pointCount; i++) { + yesSeries.push(fallbackYes); + } + } else { + for (let i = 0; i < pointCount; i++) { + const t = pointCount === 1 ? 1 : i / (pointCount - 1); + const sampleTime = startTime.getTime() + t * timeSpan; + + // Find surrounding history points for interpolation + let leftIdx = 0; + for (let j = 0; j < historyPoints.length; j++) { + if (historyPoints[j].time <= sampleTime) { + leftIdx = j; + } else { + break; + } + } + const rightIdx = Math.min(leftIdx + 1, historyPoints.length - 1); + + if (leftIdx === rightIdx) { + yesSeries.push(historyPoints[leftIdx].probability); + } else { + const leftTime = historyPoints[leftIdx].time; + const rightTime = historyPoints[rightIdx].time; + const mix = + rightTime === leftTime + ? 0 + : (sampleTime - leftTime) / (rightTime - leftTime); + const interpolated = + historyPoints[leftIdx].probability + + (historyPoints[rightIdx].probability - + historyPoints[leftIdx].probability) * + mix; + yesSeries.push(Math.max(0.02, Math.min(0.98, interpolated))); + } + } + // Snap last point to the last history entry's probability to avoid a + // visual jump when the current market.yesPrice diverges from the most + // recent on-chain transition. + yesSeries[yesSeries.length - 1] = + historyPoints[historyPoints.length - 1].probability; + } + + return { + endTime, + pointCount, + scaleHours, + startTime, + xLabelOffsets, + xLabels, + yesSeries, + }; +} diff --git a/src/components/detail.ts b/src/components/detail.ts index f3337978..ad5f03c8 100644 --- a/src/components/detail.ts +++ b/src/components/detail.ts @@ -16,7 +16,7 @@ import { import { clampContractPriceSats, getEstimatedSettlementDate, - getOrderbookLevels, + getFullOrderbook, getPathAvailability, getPositionContracts, getSelectedMarket, @@ -649,12 +649,29 @@ export function renderActionTicket(market: Market): string { -

Market execution (exact-input)

+
+ + +
+ ${ + state.orderType === "limit" + ? ` +
+ +
+ + + +
+
+ ` + : "" + }
@@ -684,6 +701,23 @@ export function renderActionTicket(market: Market): string { } ` } + ${ + state.orderType === "limit" + ? ` +
+
Order typeLimit
+
Price${Math.round(state.limitPrice * SATS_PER_FULL_CONTRACT)} sats
+
Amount${currentDirection === "buy" ? formatSats(Math.max(1, Math.floor(state.tradeSizeSats))) : `${Math.max(1, Math.floor(state.tradeContracts))} contracts`}
+
Side${state.selectedSide.toUpperCase()} · ${currentDirection}
+
+ ${ + state.tradeError + ? `

${state.tradeError}

` + : "" + } + + ` + : `

Estimated avg fill: ${preview.fill.avgPriceSats.toFixed(1)} sats (range ${preview.fill.bestPriceSats}-${preview.fill.worstPriceSats}).

${ @@ -718,6 +752,8 @@ export function renderActionTicket(market: Market): string { : "" } + ` + }
You hold: YES ${positions.yes.toFixed(2)} · NO ${positions.no.toFixed(2)} ${ @@ -733,29 +769,68 @@ export function renderActionTicket(market: Market): string { : "" }
-
- ${ - state.showOrderbook - ? `
-

${state.tradeIntent === "open" ? "Asks (buy depth)" : "Bids (sell depth)"} · ${state.selectedSide.toUpperCase()}

-
- ${getOrderbookLevels(market, state.selectedSide, state.tradeIntent) - .map( - ( - level, - idx, - ) => `
- ${level.priceSats} sats - ${level.contracts.toFixed(2)} contracts -
`, - ) - .join("")} + ${(() => { + const book = getFullOrderbook(market, state.selectedSide); + const maxContracts = Math.max( + ...book.asks.map((l) => l.contracts), + ...book.bids.map((l) => l.contracts), + 1, + ); + const askRows = [...book.asks].reverse(); + return `
+
+ Order Book + ${state.selectedSide.toUpperCase()}
-
` - : "" - } +
+ Price (sats) + Contracts +
+ ${askRows + .map((level) => { + const pct = (level.contracts / maxContracts) * 100; + return `
+
+ ${level.priceSats} + ${level.contracts.toFixed(0)} +
`; + }) + .join("")} + ${book.asks.length === 0 ? `
No asks
` : ""} +
${book.spread !== null ? `Spread: ${book.spread} sats` : "—"}
+ ${book.bids.length === 0 ? `
No bids
` : ""} + ${book.bids + .map((level) => { + const pct = (level.contracts / maxContracts) * 100; + return `
+
+ ${level.priceSats} + ${level.contracts.toFixed(0)} +
`; + }) + .join("")} +
`; + })()} + ${(() => { + const myOrders = market.limitOrders.filter( + (o) => state.nostrPubkey && o.creator_pubkey === state.nostrPubkey, + ); + if (myOrders.length === 0) return ""; + return `
+ Your Orders + ${myOrders + .map((o) => { + const cancelling = state.cancellingOrderId === o.id; + return `
+ ${o.direction_label} @ ${o.price} sats · ${o.offered_amount} offered + +
`; + }) + .join("")} +
`; + })()} ${ state.showFeeDetails ? `
@@ -765,14 +840,74 @@ export function renderActionTicket(market: Market): string {
` : "" } + ${ + state.marketMakerMode + ? `

Advanced actions

+
+
+ Active Pools + +
+ ${(() => { + const marketPools = state.myPools.filter( + (p) => p.market_id === market.marketId, + ); + if (marketPools.length === 0) + return '

No active pools for this market.

'; + return marketPools + .map( + ( + p, + ) => `
+ ${p.pool_id.slice(0, 10)}... + Y:${p.reserve_yes} N:${p.reserve_no} L:${p.reserve_collateral} + s:${p.current_s_index} +
`, + ) + .join(""); + })()} + ${ + state.poolCreateOpen + ? ` +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ ` + : "" + } +
` + : "" + } ${ - state.showAdvancedActions + state.marketMakerMode && state.showAdvancedActions ? `
@@ -924,6 +1059,7 @@ export function renderDetail(): string { ${market.resolveTx ? `
Sig hash
Resolve tx
` : ""}
${ + state.marketMakerMode && state.nostrPubkey && state.nostrPubkey === market.oraclePubkey && market.state === 1 && @@ -939,6 +1075,7 @@ export function renderDetail(): string { : "" } ${ + state.marketMakerMode && state.lastAttestationSig && state.lastAttestationMarketId === market.marketId && market.state === 1 diff --git a/src/components/home.ts b/src/components/home.ts index 7178de6a..04ca52da 100644 --- a/src/components/home.ts +++ b/src/components/home.ts @@ -63,7 +63,7 @@ export function renderHome(): string { ` : "" } - + ${state.marketMakerMode ? `` : ""} ${state.nostrPubkey ? `

Identity: ${escapeHtml(state.nostrPubkey.slice(0, 8))}...${escapeHtml(state.nostrPubkey.slice(-8))}

` : ""}
`; @@ -232,7 +232,7 @@ export function renderMyMarkets(): string {

My Markets

- + ${state.marketMakerMode ? `` : ""}
diff --git a/src/components/market-chart.ts b/src/components/market-chart.ts index 84f5da12..e4d0dbc0 100644 --- a/src/components/market-chart.ts +++ b/src/components/market-chart.ts @@ -2,7 +2,7 @@ import { SATS_PER_FULL_CONTRACT, state } from "../state.ts"; import type { Market } from "../types.ts"; import { formatEstTime } from "../utils/format.ts"; import { escapeAttr } from "../utils/html.ts"; -import { buildChartSeriesData } from "./chart-series.ts"; +import { buildChartFromHistory, buildChartSeriesData } from "./chart-series.ts"; export function chartSkeleton( market: Market, @@ -137,8 +137,13 @@ export function chartSkeleton( }; const yes = market.yesPrice ?? 0.5; + const history = state.priceHistory.get(market.marketId); + const seriesData = + history && history.length >= 2 + ? buildChartFromHistory(market, history) + : buildChartSeriesData(market); const { endTime, pointCount, startTime, xLabelOffsets, xLabels, yesSeries } = - buildChartSeriesData(market); + seriesData; const chartAspect = isHomeChart ? state.chartAspectHome : state.chartAspectDetail; diff --git a/src/components/shell.ts b/src/components/shell.ts index 37871c4e..6858dd5b 100644 --- a/src/components/shell.ts +++ b/src/components/shell.ts @@ -91,7 +91,9 @@ export function renderTopShell(): string {
${categories .filter( - (category) => category !== "My Markets" || state.nostrPubkey, + (category) => + category !== "My Markets" || + (state.nostrPubkey && state.marketMakerMode), ) .map((category) => { const active = state.activeCategory === category; @@ -274,6 +276,15 @@ export function renderTopShell(): string {
+
+
+

Market Maker mode

+

Create markets, issue tokens, resolve as oracle

+
+ +

Display Currency

Show fiat equivalents for BTC amounts

diff --git a/src/components/wallet/activity.ts b/src/components/wallet/activity.ts index 61f30625..3bff9bf9 100644 --- a/src/components/wallet/activity.ts +++ b/src/components/wallet/activity.ts @@ -10,31 +10,51 @@ import { escapeAttr, escapeHtml } from "../../utils/html.ts"; export function renderWalletTransactionRows(params: { creationTxToMarket: Map; + orderTxLabel: Map; pawIcon: string; walletData: WalletData | null; }): string { - const { creationTxToMarket, pawIcon, walletData } = params; + const { creationTxToMarket, orderTxLabel, pawIcon, walletData } = params; return (walletData?.transactions ?? []) .map((tx) => { const marketId = creationTxToMarket.get(tx.txid); const isCreation = !!marketId; const isIssuance = tx.txType === "issuance" || tx.txType === "reissuance"; + const orderInfo = orderTxLabel.get(tx.txid); + const isLimitOrder = !!orderInfo; const sign = tx.balanceChange >= 0 ? "+" : ""; - const color = - isCreation || isIssuance + const color = isLimitOrder + ? "text-amber-300" + : isCreation || isIssuance ? "text-violet-300" : tx.balanceChange >= 0 ? "text-emerald-300" : "text-red-300"; - const icon = - isCreation || isIssuance + const icon = isLimitOrder + ? "♦" + : isCreation || isIssuance ? "◆" : tx.balanceChange >= 0 ? "↓" : "↑"; let label = ""; - if (isCreation) { + if (isLimitOrder) { + const badgeContent = escapeHtml(orderInfo.label); + if (orderInfo.marketId) { + label = + '"; + } else { + label = + '' + + badgeContent + + ""; + } + } else if (isCreation) { label = '" + + "
" + + '
' + + (state.walletBalanceHidden + ? '' + + PAW_ICON + + PAW_ICON + + "" + : '' + + o.price + + " sats · " + + o.offered_amount.toLocaleString() + + " offered") + + '" + + "
" + + "
" + ); + }) + .join("")} +
+ ` + : "" + } + + ${ + state.myPools.length > 0 + ? ` + +
+

My Pools

+ ${state.myPools + .map((p) => { + return ( + '
' + + '
' + + '' + + escapeHtml(`${p.pool_id.slice(0, 10)}...`) + + "" + + '' + + escapeHtml(`${p.market_id.slice(0, 10)}...`) + + "" + + "
" + + '
' + + (state.walletBalanceHidden + ? '' + + PAW_ICON + + PAW_ICON + + "" + : 'Y:' + + p.reserve_yes + + " N:" + + p.reserve_no + + " L:" + + p.reserve_collateral + + "") + + '' + + escapeHtml(`${p.creation_txid.slice(0, 10)}...`) + + "" + + "
" + + "
" + ); + }) + .join("")} +
+ ` + : "" + } + ${utxoSection} diff --git a/src/handlers/click.ts b/src/handlers/click.ts index eab0e0bf..5ef84089 100644 --- a/src/handlers/click.ts +++ b/src/handlers/click.ts @@ -2,10 +2,15 @@ import { invoke } from "@tauri-apps/api/core"; import { openUrl } from "@tauri-apps/plugin-opener"; import { MOCK_MARKETS } from "../mock-markets.ts"; import { + cancelLimitOrder, + createLimitOrder, discoveredToMarket, executeTrade, + fetchOrders, + fetchOwnOrders, issueTokens, marketToContractParamsJson, + mergeOrdersIntoMarket, quoteTrade, } from "../services/markets.ts"; import { @@ -102,7 +107,6 @@ function currentExactInput(): number { } function enforceSizeModeForIntent(): void { - state.orderType = "market"; state.sizeMode = state.tradeIntent === "open" ? "sats" : "contracts"; } @@ -1047,6 +1051,7 @@ export async function handleClick( } if (action === "open-create-market") { + if (!state.marketMakerMode) return; state.view = "create"; render(); return; @@ -1209,6 +1214,13 @@ export async function handleClick( state.walletLoading = false; hideOverlayLoader(); render(); + // Fetch own orders for transaction labeling + fetchOwnOrders() + .then((orders) => { + state.ownOrders = orders; + render(); + }) + .catch(() => {}); // Background Electrum sync -- updates balances when done invoke("sync_wallet") .then(async () => { @@ -1341,6 +1353,15 @@ export async function handleClick( return; } + if (action === "toggle-market-maker") { + state.marketMakerMode = !state.marketMakerMode; + if (!state.marketMakerMode && state.activeCategory === "My Markets") { + state.activeCategory = "Trending"; + } + render(); + return; + } + if (action === "set-wallet-unit") { const unit = actionEl?.getAttribute("data-unit") as "sats" | "btc" | null; if (unit) { @@ -1988,6 +2009,17 @@ export async function handleClick( return; } + if (action === "toggle-pool-create") { + state.poolCreateOpen = !state.poolCreateOpen; + render(); + return; + } + + if (action === "create-pool") { + showToast("Pool creation coming soon", "info"); + return; + } + if (action === "use-cashout") { const market = getSelectedMarket(); const positions = getPositionContracts(market); @@ -2196,10 +2228,7 @@ export async function handleClick( } if (orderType) { - if (orderType === "limit") { - showToast("Limit orders are not supported in LMSR cutover yet.", "info"); - } - state.orderType = "market"; + state.orderType = orderType as OrderType; clearTradeQuoteSnapshot(); render(); return; @@ -2214,6 +2243,54 @@ export async function handleClick( return; } + if (action === "cancel-limit-order") { + const orderId = actionEl?.dataset.orderId; + + // Search all markets for the order (cancel can be triggered from wallet or detail view) + let order: import("../types.ts").DiscoveredOrder | undefined; + let orderMarketId: string | undefined; + for (const m of markets) { + const found = m.limitOrders.find((o) => o.id === orderId); + if (found) { + order = found; + orderMarketId = m.marketId; + break; + } + } + + if (!order || !orderMarketId) { + showToast("Order not found — try refreshing the page", "error"); + return; + } + + state.cancellingOrderId = orderId ?? null; + render(); + + (async () => { + try { + const result = await cancelLimitOrder(order); + showToast( + `Order cancelled! Refunded ${result.refunded_amount} sats. txid: ${result.txid.slice(0, 16)}...`, + "success", + ); + await refreshWallet(render); + const orders = await fetchOrders(orderMarketId); + mergeOrdersIntoMarket(orderMarketId, orders); + fetchOwnOrders() + .then((own) => { + state.ownOrders = own; + render(); + }) + .catch(() => {}); + } catch (error) { + showToast(`Cancel failed: ${error}`, "error"); + } + state.cancellingOrderId = null; + render(); + })(); + return; + } + if ( action === "submit-trade" || action === "submit-issue" || @@ -2279,6 +2356,54 @@ export async function handleClick( const market = getSelectedMarket(); if (action === "submit-trade") { + if (state.orderType === "limit") { + const side = state.selectedSide; + const direction = currentTradeDirection(); + const priceSats = Math.round(state.limitPrice * SATS_PER_FULL_CONTRACT); + const amount = currentExactInput(); + + if (amount <= 0) { + showToast("Enter an amount greater than zero", "error"); + return; + } + + state.tradeExecuteLoading = true; + state.tradeError = null; + render(); + + createLimitOrder(market, side, direction, priceSats, amount) + .then(async (result) => { + showToast( + `Limit order placed! txid: ${result.txid.slice(0, 16)}...`, + "success", + ); + await refreshWallet(render); + const orders = await fetchOrders(market.marketId); + mergeOrdersIntoMarket(market.marketId, orders); + fetchOwnOrders() + .then((own) => { + state.ownOrders = own; + render(); + }) + .catch(() => {}); + }) + .catch((error) => { + const msg = String(error); + if (msg.includes("excluding") && msg.includes("fee")) { + state.tradeError = + "Limit orders need two L-BTC UTXOs (one for the order, one for the fee). Send yourself a small amount first to split your balance."; + } else { + state.tradeError = msg; + } + showToast(state.tradeError, "error"); + }) + .finally(() => { + state.tradeExecuteLoading = false; + render(); + }); + return; + } + const side = state.selectedSide; const direction = currentTradeDirection(); const exactInput = currentExactInput(); diff --git a/src/lifecycle/subscriptions.ts b/src/lifecycle/subscriptions.ts index aeabbe03..5770b767 100644 --- a/src/lifecycle/subscriptions.ts +++ b/src/lifecycle/subscriptions.ts @@ -1,6 +1,10 @@ import { listen } from "@tauri-apps/api/event"; -import { refreshMarketsFromStore } from "../services/markets.ts"; -import { createWalletData, state } from "../state.ts"; +import { + fetchOrders, + mergeOrdersIntoMarket, + refreshMarketsFromStore, +} from "../services/markets.ts"; +import { createWalletData, markets, state } from "../state.ts"; import type { WalletTransaction, WalletUtxo } from "../types.ts"; export function setupTauriSubscriptions(render: () => void): () => void { @@ -83,15 +87,52 @@ export function setupTauriSubscriptions(render: () => void): () => void { }), ); + let orderRefreshInFlight = false; + let orderRefreshQueued = false; + + const scheduleOrderRefresh = (): void => { + if (disposed) return; + if (state.view !== "detail" || !state.selectedMarketId) return; + const selectedMarket = markets.find((m) => m.id === state.selectedMarketId); + if (!selectedMarket) return; + if (orderRefreshInFlight) { + orderRefreshQueued = true; + return; + } + + const marketId = selectedMarket.marketId; + orderRefreshInFlight = true; + void fetchOrders(marketId) + .then((orders) => { + if (disposed) return; + mergeOrdersIntoMarket(marketId, orders); + render(); + }) + .finally(() => { + if (disposed) return; + orderRefreshInFlight = false; + if (orderRefreshQueued) { + orderRefreshQueued = false; + scheduleOrderRefresh(); + } + }); + }; + for (const eventName of [ "discovery:market", - "discovery:order", "discovery:attestation", "discovery:pool", ]) { registerListener(listen(eventName, scheduleMarketRefresh)); } + registerListener( + listen("discovery:order", () => { + scheduleMarketRefresh(); + scheduleOrderRefresh(); + }), + ); + return () => { disposed = true; while (unlisteners.length > 0) { diff --git a/src/main.ts b/src/main.ts index a92df75c..78591af6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,13 +15,20 @@ import { handleFocusout } from "./handlers/focusout.ts"; import { handleInput } from "./handlers/input.ts"; import { handleKeydown } from "./handlers/keydown.ts"; // Services -import { loadMarkets, refreshMarketsFromStore } from "./services/markets.ts"; +import { + fetchOrders, + fetchOwnOrders, + loadMarkets, + mergeOrdersIntoMarket, + refreshMarketsFromStore, +} from "./services/markets.ts"; +import { getPriceHistory, listLmsrPools } from "./services/pools.ts"; import { fetchWalletStatus, refreshWallet, syncCurrentHeightFromLwk, } from "./services/wallet.ts"; -import { app, createWalletData, state } from "./state.ts"; +import { app, createWalletData, markets, state } from "./state.ts"; import type { IdentityResponse, NostrBackupStatus, @@ -170,6 +177,20 @@ function openMarket( state.chartHoverX = null; setLimitPriceSats(getBasePriceSats(market, nextSide)); render(); + + // Fetch limit orders for the selected market in the background + fetchOrders(market.marketId).then((orders) => { + mergeOrdersIntoMarket(market.marketId, orders); + render(); + }); + + // Load price history for the chart + getPriceHistory(market.marketId, 500) + .then((entries) => { + state.priceHistory.set(market.marketId, entries); + render(); + }) + .catch(() => {}); } async function finishOnboarding(): Promise { @@ -189,6 +210,18 @@ async function finishOnboarding(): Promise { if (state.walletStatus === "unlocked") { void refreshWallet(render); + fetchOwnOrders() + .then((orders) => { + state.ownOrders = orders; + render(); + }) + .catch(() => {}); + listLmsrPools() + .then((pools) => { + state.myPools = pools; + render(); + }) + .catch(() => {}); } await loadMarkets(); state.marketsLoading = false; @@ -337,6 +370,18 @@ async function initApp(): Promise { // 4. Normal boot — both identity and wallet exist if (state.walletStatus === "unlocked") { void refreshWallet(render); + fetchOwnOrders() + .then((orders) => { + state.ownOrders = orders; + render(); + }) + .catch(() => {}); + listLmsrPools() + .then((pools) => { + state.myPools = pools; + render(); + }) + .catch(() => {}); } await Promise.all([loadMarkets(), splashReady]); @@ -344,6 +389,14 @@ async function initApp(): Promise { render(); dismissSplash(); + // Fetch limit orders for all markets in the background + for (const m of markets) { + void fetchOrders(m.marketId).then((orders) => { + mergeOrdersIntoMarket(m.marketId, orders); + render(); + }); + } + void syncCurrentHeightFromLwk("liquid-testnet", render, updateEstClockLabels); } diff --git a/src/mock-markets.ts b/src/mock-markets.ts index f14c51f7..8ebf088b 100644 --- a/src/mock-markets.ts +++ b/src/mock-markets.ts @@ -31,7 +31,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(13), noReissuanceToken: hex64(14), creationTxid: hex64(15), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -62,7 +61,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(23), noReissuanceToken: hex64(24), creationTxid: hex64(25), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -93,7 +91,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(33), noReissuanceToken: hex64(34), creationTxid: hex64(35), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -124,7 +121,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(43), noReissuanceToken: hex64(44), creationTxid: hex64(45), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -155,7 +151,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(53), noReissuanceToken: hex64(54), creationTxid: hex64(55), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -186,7 +181,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(63), noReissuanceToken: hex64(64), creationTxid: hex64(65), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -217,7 +211,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(73), noReissuanceToken: hex64(74), creationTxid: hex64(75), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -248,7 +241,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(83), noReissuanceToken: hex64(84), creationTxid: hex64(85), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -279,7 +271,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(93), noReissuanceToken: hex64(94), creationTxid: hex64(95), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -310,7 +301,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(104), noReissuanceToken: hex64(105), creationTxid: hex64(106), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -341,7 +331,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(114), noReissuanceToken: hex64(115), creationTxid: hex64(116), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -373,7 +362,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(124), noReissuanceToken: hex64(125), creationTxid: hex64(126), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -404,7 +392,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(134), noReissuanceToken: hex64(135), creationTxid: hex64(136), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -435,7 +422,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(144), noReissuanceToken: hex64(145), creationTxid: hex64(146), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -466,7 +452,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(154), noReissuanceToken: hex64(155), creationTxid: hex64(156), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -497,7 +482,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(164), noReissuanceToken: hex64(165), creationTxid: hex64(166), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -528,7 +512,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(174), noReissuanceToken: hex64(175), creationTxid: hex64(176), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -559,7 +542,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(184), noReissuanceToken: hex64(185), creationTxid: hex64(186), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -591,7 +573,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(194), noReissuanceToken: hex64(195), creationTxid: hex64(196), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, @@ -622,7 +603,6 @@ export const MOCK_MARKETS: Market[] = [ yesReissuanceToken: hex64(204), noReissuanceToken: hex64(205), creationTxid: hex64(206), - anchor: null, collateralUtxos: [], nostrEventJson: null, anchor: null, diff --git a/src/services/markets.ts b/src/services/markets.ts index 4c0f81e7..3800b683 100644 --- a/src/services/markets.ts +++ b/src/services/markets.ts @@ -1,12 +1,16 @@ import { invoke } from "@tauri-apps/api/core"; -import { setMarkets } from "../state.ts"; +import { markets, setMarkets } from "../state.ts"; import type { + CancelLimitOrderResponse, + CreateLimitOrderResponse, DiscoveredMarket, + DiscoveredOrder, ExecuteTradeExpectedQuote, ExecuteTradeResponse, IssuanceResult, Market, MarketCategory, + OwnOrderSummary, Side, TradeDirection, TradeQuoteResponse, @@ -67,7 +71,18 @@ export async function loadMarkets(): Promise { export async function refreshMarketsFromStore(): Promise { try { const stored = await invoke("list_contracts"); - setMarkets(stored.map(discoveredToMarket)); + const oldByMarketId = new Map(markets.map((m) => [m.marketId, m])); + setMarkets( + stored.map((d) => { + const m = discoveredToMarket(d); + const prev = oldByMarketId.get(m.marketId); + if (prev) { + m.limitOrders = prev.limitOrders; + m.collateralUtxos = prev.collateralUtxos; + } + return m; + }), + ); } catch (error) { console.warn("Failed to refresh markets from store:", error); } @@ -137,3 +152,70 @@ export async function executeTrade( }, }); } + +export async function fetchOrders( + marketId?: string, +): Promise { + return invoke("fetch_orders", { + marketId: marketId ?? null, + }); +} + +export async function createLimitOrder( + market: Market, + side: Side, + direction: TradeDirection, + price: number, + amount: number, + feeAmount = 500, +): Promise { + return invoke("create_limit_order", { + request: { + contract_params_json: marketToContractParamsJson(market), + market_id: market.marketId, + side, + direction, + price: Math.floor(price), + amount: Math.floor(amount), + fee_amount: feeAmount, + }, + }); +} + +export async function cancelLimitOrder( + order: DiscoveredOrder, + orderIndex?: number, + feeAmount = 500, +): Promise { + return invoke("cancel_limit_order", { + request: { + market_id: order.market_id, + base_asset_id: order.base_asset_id, + quote_asset_id: order.quote_asset_id, + price: order.price, + min_fill_lots: order.min_fill_lots, + min_remainder_lots: order.min_remainder_lots, + direction: order.direction, + maker_base_pubkey: order.maker_base_pubkey, + order_nonce: order.order_nonce, + cosigner_pubkey: order.cosigner_pubkey, + maker_receive_spk_hash: order.maker_receive_spk_hash, + fee_amount: feeAmount, + order_index: orderIndex ?? null, + }, + }); +} + +export async function fetchOwnOrders(): Promise { + return invoke("list_own_orders"); +} + +export function mergeOrdersIntoMarket( + marketId: string, + orders: DiscoveredOrder[], +): void { + const market = markets.find((m) => m.marketId === marketId); + if (market) { + market.limitOrders = orders; + } +} diff --git a/src/services/pools.ts b/src/services/pools.ts new file mode 100644 index 00000000..19130ade --- /dev/null +++ b/src/services/pools.ts @@ -0,0 +1,69 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { + CreateLmsrPoolResponse, + LmsrPoolInfo, + PriceHistoryEntry, + ScanLmsrPoolResponse, +} from "../types.ts"; + +export async function generateLmsrTable( + liquidityParam: number, + tableDepth: number, + qStepLots: number, + sBias: number, + halfPayoutSats: number, +): Promise { + return invoke("generate_lmsr_table", { + liquidityParam, + tableDepth, + qStepLots, + sBias, + halfPayoutSats, + }); +} + +export async function createLmsrPool( + marketParamsJson: string, + poolParamsJson: string, + initialSIndex: number, + initialReservesYes: number, + initialReservesNo: number, + initialReservesLbtc: number, + tableValues: number[], + feeAmount?: number, +): Promise { + return invoke("create_lmsr_pool", { + request: { + market_params_json: marketParamsJson, + pool_params_json: poolParamsJson, + initial_s_index: initialSIndex, + initial_reserves_yes: initialReservesYes, + initial_reserves_no: initialReservesNo, + initial_reserves_lbtc: initialReservesLbtc, + table_values: tableValues, + fee_amount: feeAmount, + }, + }); +} + +export async function scanLmsrPool( + poolId: string, +): Promise { + return invoke("scan_lmsr_pool", { poolId }); +} + +export async function listLmsrPools( + marketId?: string, +): Promise { + return invoke("list_lmsr_pools", { marketId }); +} + +export async function getPriceHistory( + marketId: string, + limit?: number, +): Promise { + return invoke("get_price_history", { + marketId, + limit, + }); +} diff --git a/src/state.ts b/src/state.ts index 2be91c51..2d252a7e 100644 --- a/src/state.ts +++ b/src/state.ts @@ -7,12 +7,15 @@ import type { BoltzLightningReceiveCreated, BoltzSubmarineSwapCreated, LimitSellWarning, + LmsrPoolInfo, Market, MarketCategory, NavCategory, NostrBackupStatus, NostrProfile, OrderType, + OwnOrderSummary, + PriceHistoryEntry, QuoteMarketTradeResult, RelayEntry, Side, @@ -112,6 +115,7 @@ export const state: { showFeeDetails: boolean; tradeQuoteLoading: boolean; tradeExecuteLoading: boolean; + cancellingOrderId: string | null; tradeQuoteSnapshot: TradeQuoteSnapshot | null; tradeError: string | null; tradeSizeSats: number; @@ -182,6 +186,7 @@ export const state: { walletBalanceHidden: boolean; showMiniWallet: boolean; showLbtcLabel: boolean; + marketMakerMode: boolean; baseCurrency: BaseCurrency; helpOpen: boolean; settingsOpen: boolean; @@ -234,6 +239,10 @@ export const state: { chartHoverMarketId: string | null; chartHoverX: number | null; chartTimescale: "1H" | "3H" | "6H" | "12H" | "1D"; + ownOrders: OwnOrderSummary[]; + myPools: LmsrPoolInfo[]; + poolCreateOpen: boolean; + priceHistory: Map; chartAspectHome: number; chartAspectDetail: number; } = { @@ -253,6 +262,7 @@ export const state: { showFeeDetails: false, tradeQuoteLoading: false, tradeExecuteLoading: false, + cancellingOrderId: null, tradeQuoteSnapshot: null, tradeError: null, tradeSizeSats: 10000, @@ -327,6 +337,7 @@ export const state: { walletBalanceHidden: false, showMiniWallet: true, showLbtcLabel: false, + marketMakerMode: false, baseCurrency: "BTC" as BaseCurrency, marketCreating: false, helpOpen: false, @@ -384,6 +395,10 @@ export const state: { chartHoverMarketId: null, chartHoverX: null, chartTimescale: "3H", + ownOrders: [], + myPools: [], + poolCreateOpen: false, + priceHistory: new Map(), chartAspectHome: 2.8, chartAspectDetail: 3.35, }; diff --git a/src/types.ts b/src/types.ts index aaefc27b..3193f193 100644 --- a/src/types.ts +++ b/src/types.ts @@ -272,6 +272,12 @@ export type OrderbookLevel = { contracts: number; }; +export type FullOrderbook = { + asks: OrderbookLevel[]; + bids: OrderbookLevel[]; + spread: number | null; +}; + export type FillEstimate = { avgPriceSats: number; bestPriceSats: number; @@ -361,6 +367,28 @@ export type ExecuteTradeResponse = { } | null; }; +export type CreateLimitOrderResponse = { + txid: string; + nostr_event_id: string; + covenant_address: string; + order_amount: number; + order_index: number; +}; + +export type CancelLimitOrderResponse = { + txid: string; + refunded_amount: number; +}; + +export type OwnOrderSummary = { + creation_txid: string | null; + market_id: string | null; + direction_label: string | null; + price: number; + offered_amount: number | null; + order_status: string; +}; + export type TradeQuoteSnapshot = { marketId: string; side: Side; @@ -368,3 +396,52 @@ export type TradeQuoteSnapshot = { exactInput: number; quote: TradeQuoteResponse; }; + +// LMSR Pool types +export type LmsrPoolInfo = { + pool_id: string; + market_id: string; + creation_txid: string; + current_s_index: number; + reserve_yes: number; + reserve_no: number; + reserve_collateral: number; + state_source: string; + params_json: string; + created_at: string; + updated_at: string; +}; + +export type PriceHistoryEntry = { + pool_id: string; + market_id: string; + transition_txid: string; + old_s_index: number; + new_s_index: number; + reserve_yes: number; + reserve_no: number; + reserve_collateral: number; + implied_yes_price_bps: number; + recorded_at: string; + block_height: number | null; +}; + +export type CreateLmsrPoolResponse = { + txid: string; + pool_id: string; +}; + +export type ScanLmsrPoolResponse = { + pool_id: string; + current_s_index: number; + reserve_yes: number; + reserve_no: number; + reserve_collateral: number; +}; + +export type CloseLmsrPoolResponse = { + txid: string; + reclaimed_yes: number; + reclaimed_no: number; + reclaimed_collateral: number; +}; diff --git a/src/utils/market.ts b/src/utils/market.ts index e2f6a0af..9cd9af7b 100644 --- a/src/utils/market.ts +++ b/src/utils/market.ts @@ -9,6 +9,7 @@ import type { CovenantState, DiscoveredOrder, FillEstimate, + FullOrderbook, LimitSellWarning, Market, OrderbookLevel, @@ -142,19 +143,7 @@ export function getOrderbookLevels( side: Side, intent: TradeIntent, ): OrderbookLevel[] { - const discovered = getDiscoveredOrderbookLevels(market, side, intent); - if (discovered.length > 0) { - return discovered; - } - - const seed = getMarketSeed(market); - const base = getBasePriceSats(market, side); - return Array.from({ length: 8 }).map((_, idx) => { - const offset = intent === "open" ? idx + 1 : -(idx + 1); - const priceSats = clampContractPriceSats(base + offset); - const contracts = 12 + ((seed + idx * 11) % 34); - return { priceSats, contracts }; - }); + return getDiscoveredOrderbookLevels(market, side, intent); } export function getDiscoveredOrderbookLevels( @@ -189,6 +178,23 @@ export function getDiscoveredOrderbookLevels( return []; } +export function getFullOrderbook(market: Market, side: Side): FullOrderbook { + const asks = getDiscoveredOrderbookLevels(market, side, "open"); + const bids = getDiscoveredOrderbookLevels(market, side, "close"); + + // asks: ascending (best ask = lowest price first) + asks.sort((a, b) => a.priceSats - b.priceSats); + // bids: descending (best bid = highest price first) + bids.sort((a, b) => b.priceSats - a.priceSats); + + const bestAsk = asks[0]?.priceSats ?? null; + const bestBid = bids[0]?.priceSats ?? null; + const spread = + bestAsk !== null && bestBid !== null ? bestAsk - bestBid : null; + + return { asks, bids, spread }; +} + export function getLimitSellWarning( limitPriceSats: number, referencePriceSats: number | null,