diff --git a/.gitignore b/.gitignore index aaab2d0..c67fac1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .vscode/ .cache .env +config.toml # Debugging data logs.md diff --git a/Cargo.lock b/Cargo.lock index e5ce080..777de86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,15 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arrayvec" @@ -96,6 +90,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "base58ck" version = "0.1.0" @@ -106,6 +106,12 @@ dependencies = [ "bitcoin_hashes", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -159,6 +165,7 @@ dependencies = [ "hex-conservative", "hex_lit", "secp256k1", + "serde", ] [[package]] @@ -166,6 +173,9 @@ name = "bitcoin-internals" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] [[package]] name = "bitcoin-io" @@ -186,6 +196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ "bitcoin-internals", + "serde", ] [[package]] @@ -196,16 +207,57 @@ checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoind" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6620b7c942dbe28cc49c21d95e792feb9ffd95a093205e7875ccfa69c2925" +dependencies = [ + "anyhow", + "bitcoincore-rpc", + "log", + "tempfile", + "which", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] [[package]] name = "block-buffer" @@ -216,6 +268,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -302,55 +363,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "config" -version = "0.15.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" -dependencies = [ - "async-trait", - "convert_case", - "json5", - "pathdiff", - "ron", - "rust-ini", - "serde-untagged", - "serde_core", - "serde_json", - "toml", - "winnow", - "yaml-rust2", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -360,12 +372,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.7" @@ -376,6 +382,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix 0.31.1", + "windows-sys 0.61.2", +] + [[package]] name = "digest" version = "0.10.7" @@ -386,6 +403,18 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -397,15 +426,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "dotenvy" version = "0.15.7" @@ -422,23 +442,39 @@ dependencies = [ ] [[package]] -name = "elements" -version = "0.25.2" +name = "electrsd" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" +checksum = "91435161fb2ad5098e7ac7a4b793bf9c34723b0208a3fcf6f33707489e771396" +dependencies = [ + "bitcoind", + "electrum-client", + "log", + "nix 0.25.1", + "which", +] + +[[package]] +name = "electrum-client" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0bd443023f9f5c4b7153053721939accc7113cbdf810a024434eed454b3db1" dependencies = [ - "bech32", "bitcoin", - "secp256k1-zkp", + "log", + "serde", + "serde_json", ] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "elements" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" dependencies = [ - "cfg-if", + "bech32", + "bitcoin", + "secp256k1-zkp", ] [[package]] @@ -447,17 +483,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - [[package]] name = "errno" version = "0.3.14" @@ -468,6 +493,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -560,6 +591,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "ghost-cell" version = "0.2.6" @@ -572,12 +616,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.5" @@ -593,15 +631,6 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "heck" version = "0.5.0" @@ -639,6 +668,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -820,6 +858,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -849,6 +893,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -899,14 +945,15 @@ dependencies = [ ] [[package]] -name = "json5" -version = "0.4.1" +name = "jsonrpc" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" dependencies = [ - "pest", - "pest_derive", + "base64 0.13.1", + "minreq", "serde", + "serde_json", ] [[package]] @@ -914,9 +961,12 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" @@ -924,6 +974,18 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.1" @@ -966,6 +1028,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "miniscript" version = "12.3.5" @@ -1000,6 +1071,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1009,6 +1106,21 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell" version = "1.21.3" @@ -1021,16 +1133,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - [[package]] name = "outref" version = "0.5.2" @@ -1060,12 +1162,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1145,6 +1241,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1313,7 +1419,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -1398,34 +1504,36 @@ dependencies = [ ] [[package]] -name = "ron" -version = "0.12.0" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" -dependencies = [ - "bitflags", - "once_cell", - "serde", - "serde_derive", - "typeid", - "unicode-ident", -] +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] -name = "rust-ini" -version = "0.21.3" +name = "rustix" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "cfg-if", - "ordered-multimap", + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", ] [[package]] -name = "rustc-hash" -version = "2.1.1" +name = "rustix" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] [[package]] name = "rustls" @@ -1530,6 +1638,7 @@ dependencies = [ "bitcoin_hashes", "rand 0.8.5", "secp256k1-sys", + "serde", ] [[package]] @@ -1563,6 +1672,12 @@ dependencies = [ "secp256k1-sys", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1573,18 +1688,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -1679,32 +1782,57 @@ dependencies = [ name = "simplex" version = "0.1.0" dependencies = [ + "anyhow", "bincode", "either", "serde", "simplex-core", "simplex-macros", + "simplex-runtime", + "simplex-test", "simplicityhl", + "tokio", "trybuild", ] +[[package]] +name = "simplex-build" +version = "0.1.0" +dependencies = [ + "simplex-sdk", + "simplicityhl", + "thiserror", +] + [[package]] name = "simplex-cli" version = "0.1.0" dependencies = [ "anyhow", "clap", - "config", + "ctrlc", "dotenvy", - "serde", + "electrsd", + "simplex-config", + "simplex-test", "simplicityhl", "thiserror", "tokio", - "toml", "tracing", "tracing-subscriber", ] +[[package]] +name = "simplex-config" +version = "0.1.0" +dependencies = [ + "serde", + "simplex-core", + "simplicityhl", + "thiserror", + "toml", +] + [[package]] name = "simplex-core" version = "0.1.0" @@ -1719,41 +1847,65 @@ dependencies = [ ] [[package]] -name = "simplex-explorer" +name = "simplex-macro-core" +version = "0.1.0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "simplicityhl", + "syn 2.0.114", + "thiserror", +] + +[[package]] +name = "simplex-macros" +version = "0.1.0" +dependencies = [ + "serde", + "simplex-macro-core", + "syn 2.0.114", +] + +[[package]] +name = "simplex-runtime" version = "0.1.0" dependencies = [ "async-trait", "bitcoin_hashes", + "electrsd", "hex-simd", - "lazy_static", "reqwest", "serde", "serde_json", + "simplex-core", "simplicityhl", "thiserror", "tokio", - "url", ] [[package]] -name = "simplex-macro-core" +name = "simplex-sdk" version = "0.1.0" dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", + "minreq", + "sha2", + "simplex-runtime", "simplicityhl", - "syn 2.0.114", "thiserror", ] [[package]] -name = "simplex-macros" +name = "simplex-test" version = "0.1.0" dependencies = [ - "serde", - "simplex-macro-core", - "syn 2.0.114", + "electrsd", + "simplex-config", + "simplex-core", + "simplex-runtime", + "simplex-sdk", + "simplicityhl", + "thiserror", ] [[package]] @@ -1823,12 +1975,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1894,6 +2040,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1932,15 +2091,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinystr" version = "0.8.2" @@ -2064,7 +2214,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -2170,12 +2320,6 @@ dependencies = [ "toml", ] -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" version = "1.19.0" @@ -2195,10 +2339,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] -name = "unicode-segmentation" -version = "1.12.0" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -2284,6 +2428,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -2343,6 +2496,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -2378,6 +2565,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -2554,33 +2753,101 @@ name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "writeable" -version = "0.6.2" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] [[package]] -name = "yaml-rust2" -version = "0.10.4" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", ] +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 59f89c7..2b7afd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,24 @@ multiple_crate_versions = "allow" [workspace.dependencies] simplex-core = { path = "./crates/core" } +simplex-runtime = { path = "./crates/runtime" } +simplex-macros = { path = "./crates/macros" } +simplex-macros-core = { path = "./crates/macros-core" } +simplex-test = { path = "./crates/test" } +simplex-sdk = { path = "./crates/sdk" } +simplex-config = { path = "./crates/config" } +simplex = { path = "./crates/simplex" } bincode = { version = "2.0.1", features = ["serde"] } ring = { version = "0.17.14" } sha2 = { version = "0.10.9", features = ["compress"] } +thiserror = { version = "2.0.18" } hex = { version = "0.4.3" } tracing = { version = "0.1.41" } minreq = { version = "2.14.1", features = ["https", "json-using-serde"] } +electrsd = { version = "0.29.0", features = ["legacy"] } simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } diff --git a/Simplex.example.toml b/Simplex.example.toml new file mode 100644 index 0000000..54129ce --- /dev/null +++ b/Simplex.example.toml @@ -0,0 +1 @@ +network = "liquid" \ No newline at end of file diff --git a/assets/elementsd b/assets/elementsd new file mode 100755 index 0000000..30a7b82 Binary files /dev/null and b/assets/elementsd differ diff --git a/crates/artifacts/Cargo.toml b/crates/artifacts/Cargo.toml new file mode 100644 index 0000000..c50113f --- /dev/null +++ b/crates/artifacts/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "simplex-build" +version = "0.1.0" +edition = "2024" +description = "Simplex Build" +license = "MIT OR Apache-2.0" +readme = "README.md" + +[lints] +workspace = true + +[dependencies] +thiserror = { workspace = true } + +simplex-sdk = { workspace = true } +simplicityhl = { workspace = true } diff --git a/crates/artifacts/src/asset_auth.rs b/crates/artifacts/src/asset_auth.rs new file mode 100644 index 0000000..fea1b84 --- /dev/null +++ b/crates/artifacts/src/asset_auth.rs @@ -0,0 +1,89 @@ +use simplex_sdk::arguments::ArgumentsTrait; +use simplex_sdk::program::Program; + +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; + +// use simplex_macros::simplex_build; + +// #[derive(SimplexBuild)] +// #[simplex("../../contracts/src/asset_auth/source_simf/asset_auth.simf")] +pub struct AssetAuth<'a> { + program: Program<'a>, +} + +impl<'a> AssetAuth<'a> { + // the path is autogenerated + pub const SOURCE: &'static str = ""; + // include_str!("../../contracts/src/asset_auth/source_simf/asset_auth.simf"); + + pub fn new(public_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { + Self { + program: Program::new(Self::SOURCE, public_key, arguments), + } + } + + pub fn get_program(&self) -> &Program<'a> { + &self.program + } +} + +// Expanded by macro + +pub mod asset_auth_build { + use simplex_sdk::arguments::ArgumentsTrait; + use simplex_sdk::witness::WitnessTrait; + use simplicityhl::value::UIntValue; + use simplicityhl::value::ValueConstructible; + use simplicityhl::{Value, WitnessValues}; + use std::collections::HashMap; + + pub struct AssetAuthWitness { + pub path: (bool, u64, u64), + } + + pub struct AssetAuthArguments { + pub first: u64, + pub second: bool, + } + + impl WitnessTrait for AssetAuthWitness { + fn build_witness(&self) -> WitnessValues { + WitnessValues::from(HashMap::from([( + simplicityhl::str::WitnessName::from_str_unchecked("PATH"), + Value::tuple([ + Value::from(self.path.0), + Value::from(UIntValue::U64(self.path.1)), + Value::from(UIntValue::U64(self.path.1)), + ]), + )])) + } + + // fn from_witness(_witness: &::simplicityhl::WitnessValues) -> Self { + // Self { + // path: (false, 0, 0), + // } + // } + } + + impl ArgumentsTrait for AssetAuthArguments { + fn build_arguments(&self) -> simplicityhl::Arguments { + simplicityhl::Arguments::from(HashMap::from([ + ( + simplicityhl::str::WitnessName::from_str_unchecked("FIRST"), + Value::from(UIntValue::U64(self.first)), + ), + ( + simplicityhl::str::WitnessName::from_str_unchecked("SECOND"), + Value::from(self.second), + ), + ])) + } + + // fn from_arguments(_args: &simplicityhl::Arguments) -> Self { + // Self { + // first: 0, + // second: false, + // } + // } + } +} diff --git a/crates/artifacts/src/lib.rs b/crates/artifacts/src/lib.rs new file mode 100644 index 0000000..c37f1ff --- /dev/null +++ b/crates/artifacts/src/lib.rs @@ -0,0 +1 @@ +pub mod asset_auth; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index b8183f5..a5401d6 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -15,14 +15,16 @@ path = "src/bin/main.rs" workspace = true [dependencies] +simplex-test = { workspace = true } +simplex-config = { workspace = true } + anyhow = "1" dotenvy = "0.15" clap = { version = "4", features = ["derive", "env"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -config = { version = "0.15.16", default-features = true } -toml = { version = "0.9.8" } -serde = { version = "1", features = ["derive"] } simplicityhl = { workspace = true } tracing = { version = "0.1.44" } -thiserror = { version = "2.0.18" } -tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } \ No newline at end of file +thiserror = { workspace = true } +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +ctrlc = { version = "3.5.2", features = ["termination"] } +electrsd = { workspace = true } \ No newline at end of file diff --git a/crates/cli/src/cli/commands.rs b/crates/cli/src/cli/commands.rs index ec56e8b..75b0c7a 100644 --- a/crates/cli/src/cli/commands.rs +++ b/crates/cli/src/cli/commands.rs @@ -4,4 +4,6 @@ use clap::Subcommand; pub enum Command { /// Show current configuration Config, + /// Launch `elementsd` in regtest mode with a default config + Regtest, } diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 1260bea..35d990b 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -1,13 +1,14 @@ pub mod commands; use crate::error::Error; - -use crate::config::{Config, default_config_path}; - use clap::Parser; +use simplex_config::Config; +use simplex_test::TestProvider; use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; -pub use commands::Command; +const DEFAULT_CONFIG_PATH: &str = "config.toml"; #[derive(Debug, Parser)] #[command(name = "simplicity-dex")] @@ -17,7 +18,7 @@ pub struct Cli { pub config: PathBuf, #[command(subcommand)] - pub command: Command, + pub command: commands::Command, } impl Cli { @@ -34,10 +35,38 @@ impl Cli { let config = self.load_config(); match &self.command { - Command::Config => { + commands::Command::Config => { println!("{config:#?}"); Ok(()) } + commands::Command::Regtest => { + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .expect("Error setting Ctrl-C handler"); + + let mut node = TestProvider::create_default_node_with_stdin(); + + println!("======================================"); + println!("Waiting for Ctrl-C..."); + println!("url: {}", node.rpc_url()); + let cookie_values = node.params.get_cookie_values()?.unwrap(); + println!("user: {:?}, password: {:?}", cookie_values.user, cookie_values.password); + println!("======================================"); + + while running.load(Ordering::SeqCst) {} + let _ = node.stop(); + println!("Exiting..."); + Ok(()) + } } } } + +#[must_use] +pub fn default_config_path() -> PathBuf { + PathBuf::from(DEFAULT_CONFIG_PATH) +} diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs deleted file mode 100644 index 19ba234..0000000 --- a/crates/cli/src/config.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::error::Error; -use serde::{Deserialize, Serialize}; -use simplicityhl::elements::AddressParams; -use std::path::{Path, PathBuf}; - -const DEFAULT_CONFIG_PATH: &str = "config.toml"; - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct Config { - #[serde(default)] - pub network: NetworkConfig, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NetworkConfig { - #[serde(default = "default_network")] - pub name: NetworkName, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "lowercase")] -pub enum NetworkName { - #[default] - Testnet, - Mainnet, -} - -impl NetworkName { - #[must_use] - pub const fn address_params(self) -> &'static AddressParams { - match self { - Self::Testnet => &AddressParams::LIQUID_TESTNET, - Self::Mainnet => &AddressParams::LIQUID, - } - } -} - -impl Config { - /// Loads configuration from the specified path. - /// - /// # Errors - /// Returns `Error::Io` if the file cannot be read, or `Error::TomlParse` if the content - /// is not valid TOML. - pub fn load(path: impl AsRef) -> Result { - let content = std::fs::read_to_string(path)?; - let config: Self = toml::from_str(&content)?; - Ok(config) - } - - pub fn load_or_default(path: impl AsRef) -> Self { - Self::load(path).unwrap_or_default() - } - - #[must_use] - pub const fn address_params(&self) -> &'static AddressParams { - self.network.name.address_params() - } -} - -impl Default for NetworkConfig { - fn default() -> Self { - Self { - name: default_network(), - } - } -} - -const fn default_network() -> NetworkName { - NetworkName::Testnet -} - -#[must_use] -pub fn default_config_path() -> PathBuf { - PathBuf::from(DEFAULT_CONFIG_PATH) -} diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index e6a70ef..02f0125 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -11,10 +11,6 @@ pub enum Error { #[error("IO error: {0}")] Io(#[from] std::io::Error), - /// Errors when parsing TOML configuration files. - #[error("TOML parse error: {0}")] - TomlParse(#[from] toml::de::Error), - /// Errors related to Partially Signed Elements Transactions (PSET). #[error("PSET error: {0}")] Pset(#[from] simplicityhl::elements::pset::Error), @@ -22,4 +18,12 @@ pub enum Error { /// Errors when converting hex strings to byte arrays. #[error("Hex to array error: {0}")] HexToArray(#[from] HexToArrayError), + + /// Errors when using test suite to run elementsd node in regtest. + #[error("Occurred error with test suite, error: {0}")] + Test(#[from] Box), + + /// Errors when building config. + #[error("Occurred error with config building, error: {0}")] + ConfigError(#[from] simplex_config::ConfigError), } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index c7a1415..4e66fdb 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,4 +1,3 @@ pub mod cli; -pub mod config; pub mod error; pub mod logging; diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml new file mode 100644 index 0000000..55cf923 --- /dev/null +++ b/crates/config/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "simplex-config" +version = "0.1.0" +license.workspace = true +edition.workspace = true + + +[lints] +workspace = true + + +[dependencies] +simplex-core = { workspace = true } + +simplicityhl = { workspace = true } +serde = { version = "1.0.228" } +thiserror = { workspace = true } +toml = { version = "0.9.8" } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs new file mode 100644 index 0000000..e42f44a --- /dev/null +++ b/crates/config/src/lib.rs @@ -0,0 +1,159 @@ +use serde::{Deserialize, Serialize}; +use simplex_core::SimplicityNetwork; +use std::fmt::Display; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +const MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR"; +const CONFIG_FILENAME: &str = "Simplex.toml"; + +#[derive(thiserror::Error, Debug)] +pub enum ConfigError { + /// Standard I/O errors. + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + /// Errors when parsing TOML configuration files. + #[error("TOML parse error: {0}")] + TomlParse(#[from] toml::de::Error), + + /// Errors when parsing TOML configuration files. + #[error("Unable to deserialize config: {0}")] + UnableToDeserialize(toml::de::Error), + + /// Errors when parsing env variable. + #[error("Unable to get env variable: {0}")] + UnableToGetEnv(#[from] std::env::VarError), + + /// Errors when getting a path to config. + #[error("Path doesn't a file: '{0}'")] + PathIsNotFile(PathBuf), + + /// Errors when getting a path to config. + #[error("Path doesn't exist: '{0}'")] + PathIsNotEsixt(PathBuf), +} + +#[derive(Debug, Default, Clone)] +pub struct Config { + pub provider_config: ProviderConfig, + pub test_config: TestConfig, +} + +#[derive(Debug, Clone)] +pub struct ProviderConfig { + simplicity_network: SimplicityNetwork, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct TestConfig { + pub rpc_creds: RpcCreds, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub enum RpcCreds { + Auth { + rpc_username: String, + rpc_password: String, + }, + #[default] + None, +} + +#[derive(Debug, Default, Clone)] +pub struct ConfigOverride { + pub rpc_creds: Option, + pub network: Option, +} + +impl Default for ProviderConfig { + fn default() -> Self { + ProviderConfig { + simplicity_network: SimplicityNetwork::LiquidTestnet, + } + } +} + +impl Config { + pub fn discover(cfg_override: &ConfigOverride) -> Result, ConfigError> { + Config::_discover().map(|opt| { + opt.map(|mut cfg| { + if let Some(test_conf) = cfg_override.rpc_creds.clone() { + cfg.test_config = test_conf; + } + if let Some(network) = cfg_override.network { + cfg.provider_config.simplicity_network = network; + } + cfg + }) + }) + } + + pub fn load_or_default(path_buf: impl AsRef) -> Self { + Self::from_path(path_buf).unwrap_or_else(|_| { + if let Ok(Some(conf)) = Self::_discover() { + conf + } else { + Self::default() + } + }) + } + + fn _discover() -> Result, ConfigError> { + let path = std::env::var(MANIFEST_DIR)?; + let path = PathBuf::from_str(&path).unwrap(); + let path = path.join(CONFIG_FILENAME); + dbg!(&path); + if !path.is_file() { + return Err(ConfigError::PathIsNotFile(path)); + } + if !path.exists() { + return Err(ConfigError::PathIsNotEsixt(path)); + } + dbg!(3); + Ok(Some(Config::from_path(&path)?)) + } + + fn from_path(p: impl AsRef) -> Result { + std::fs::read_to_string(p.as_ref())?.parse() + } +} + +impl FromStr for Config { + type Err = ConfigError; + + fn from_str(s: &str) -> Result { + let cfg: _Config = toml::from_str(s).map_err(ConfigError::UnableToDeserialize)?; + Ok(Config { + provider_config: ProviderConfig { + simplicity_network: cfg.network.unwrap_or_default().into(), + }, + test_config: cfg.test.unwrap_or_default(), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct _Config { + network: Option<_NetworkName>, + test: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +enum _NetworkName { + #[default] + Liquid, + LiquidTestnet, + ElementsRegtest, +} + +impl Into for _NetworkName { + fn into(self) -> SimplicityNetwork { + match self { + _NetworkName::Liquid => SimplicityNetwork::Liquid, + _NetworkName::LiquidTestnet => SimplicityNetwork::LiquidTestnet, + _NetworkName::ElementsRegtest => SimplicityNetwork::default_regtest(), + } + } +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index fd2dcf0..513aead 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -19,7 +19,7 @@ workspace = true encoding = ["dep:bincode"] [dependencies] -thiserror = { version = "2.0.18" } +thiserror = { workspace = true } bincode = { workspace = true, optional = true } sha2 = { workspace = true } hex = { workspace = true } diff --git a/crates/explorer/src/lib.rs b/crates/explorer/src/lib.rs deleted file mode 100644 index dd55acf..0000000 --- a/crates/explorer/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod error; -pub mod esplora; -mod traits; -pub mod waterfall; - -pub use esplora::EsploraClient; -// pub use waterfall::WaterfallClient; diff --git a/crates/explorer/src/traits.rs b/crates/explorer/src/traits.rs deleted file mode 100644 index 8b13789..0000000 --- a/crates/explorer/src/traits.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/macros-core/Cargo.toml b/crates/macros-core/Cargo.toml index 964592d..f8e1b29 100644 --- a/crates/macros-core/Cargo.toml +++ b/crates/macros-core/Cargo.toml @@ -13,12 +13,11 @@ bincode = [] serde = ["bincode"] default = ["bincode", "serde"] - [dependencies] proc-macro-error = { version = "1.0" } proc-macro2 = { version = "1.0.106", features = ["span-locations"] } syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } -thiserror = { version = "2.0.18" } +thiserror = { workspace = true } quote = { version = "1.0.44" } simplicityhl = { workspace = true } diff --git a/crates/macros-core/src/lib.rs b/crates/macros-core/src/lib.rs index 17cc241..8132d6a 100644 --- a/crates/macros-core/src/lib.rs +++ b/crates/macros-core/src/lib.rs @@ -20,6 +20,28 @@ pub fn expand_include_simf(input: &attr::parse::SynFilePath) -> syn::Result syn::Result { - todo!() +pub fn expand_test(_args: &proc_macro2::TokenStream, input: &syn::ItemFn) -> syn::Result { + // TODO: maybe check crate attributes to allow user to do smth like in sqlx? + Ok(expand_simple(input)) +} + +fn expand_simple(input: &syn::ItemFn) -> proc_macro2::TokenStream { + let ret = &input.sig.output; + let name = &input.sig.ident; + let body = &input.block; + let attrs = &input.attrs; + + let fn_name_str = name.to_string(); + let ident = format!("{input:#?}"); + quote::quote! { + #[::core::prelude::v1::test] + #(#attrs)* + fn #name() #ret { + #body + // ::sqlx::test_block_on(async { #body }) + // before + println!("Running test: {}, \n -- {}", #fn_name_str, #ident); + //revert + } + } } diff --git a/crates/explorer/Cargo.toml b/crates/runtime/Cargo.toml similarity index 79% rename from crates/explorer/Cargo.toml rename to crates/runtime/Cargo.toml index 174d99f..08fe472 100644 --- a/crates/explorer/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "simplex-explorer" +name = "simplex-runtime" version = "0.1.0" license.workspace = true edition.workspace = true @@ -20,13 +20,13 @@ workspace = true async-trait = { version = "0.1.89" } simplicityhl = { workspace = true } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -thiserror = { version = "2.0.18" } +thiserror = { workspace = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -url = { version = "2.5.8" } -lazy_static = { version = "1.5.0", features = ["spin_no_std"] } hex-simd = "0.8.0" bitcoin_hashes = "0.14.1" +simplex-core = { workspace = true } +electrsd = { workspace = true } [dev-dependencies] -tokio = {version = "1.49.0", features = ["full"]} +tokio = { version = "1.49.0", features = ["full"] } diff --git a/crates/explorer/README.md b/crates/runtime/README.md similarity index 100% rename from crates/explorer/README.md rename to crates/runtime/README.md diff --git a/crates/explorer/api-esplora.md b/crates/runtime/api-esplora.md similarity index 100% rename from crates/explorer/api-esplora.md rename to crates/runtime/api-esplora.md diff --git a/crates/explorer/api-waterfall.md b/crates/runtime/api-waterfall.md similarity index 100% rename from crates/explorer/api-waterfall.md rename to crates/runtime/api-waterfall.md diff --git a/crates/runtime/src/elements_rpc/mod.rs b/crates/runtime/src/elements_rpc/mod.rs new file mode 100644 index 0000000..00ef807 --- /dev/null +++ b/crates/runtime/src/elements_rpc/mod.rs @@ -0,0 +1,376 @@ +mod types; + +pub use types::*; + +use crate::error::ExplorerError; +use bitcoind::bitcoincore_rpc::{Auth, Client, RpcApi, bitcoin}; +use electrsd::bitcoind; +use serde_json::Value; +use simplex_core::SimplicityNetwork; +use simplicityhl::elements::{Address, AssetId, BlockHash, Txid}; +use std::str::FromStr; + +pub struct ElementsRpcClient { + inner: Client, + #[allow(unused)] + network: SimplicityNetwork, + #[allow(unused)] + auth: Auth, + #[allow(unused)] + url: String, +} + +impl ElementsRpcClient { + pub fn new(network: SimplicityNetwork, url: &str, auth: Auth) -> Result { + let inner = Client::new(url, auth.clone())?; + inner.ping()?; + Ok(Self { + inner, + network, + auth, + url: url.to_string(), + }) + } + + pub fn new_from_credentials( + network: SimplicityNetwork, + url: &str, + user: &str, + pass: &str, + ) -> Result { + let auth = Auth::UserPass(user.to_string(), pass.to_string()); + Self::new(network, url, auth) + } + + pub fn client(&self) -> &Client { + &self.inner + } + + pub fn network(&self) -> SimplicityNetwork { + self.network + } +} + +impl ElementsRpcClient { + pub fn height(client: &Client) -> Result { + const METHOD: &str = "getblockcount"; + + client + .call::(METHOD, &[])? + .as_u64() + .ok_or_else(|| ExplorerError::ElementsRpcUnexpectedReturn(METHOD.into())) + } + + pub fn blockchain_info(client: &Client) -> Result { + const METHOD: &str = "getblockchaininfo"; + + Ok(client.call::(METHOD, &[])?) + } + + pub fn sendtoaddress( + client: &Client, + address: &Address, + satoshi: u64, + asset: Option, + ) -> Result { + const METHOD: &str = "sendtoaddress"; + + let btc = sat2btc(satoshi); + let r = match asset { + Some(asset) => client.call::( + METHOD, + &[ + address.to_string().into(), + btc.into(), + "".into(), + "".into(), + false.into(), + false.into(), + 1.into(), + "UNSET".into(), + false.into(), + asset.to_string().into(), + ], + )?, + None => client.call::(METHOD, &[address.to_string().into(), btc.into()])?, + }; + Ok(Txid::from_str(r.as_str().unwrap()).unwrap()) + } + + pub fn rescanblockchain(client: &Client, start: Option, stop: Option) -> Result<(), ExplorerError> { + const METHOD: &str = "rescanblockchain"; + + let mut args = Vec::with_capacity(2); + if start.is_some() { + args.push(start.into()) + } + if stop.is_some() { + args.push(stop.into()) + } + client.call::(METHOD, &args)?; + Ok(()) + } + + pub fn getnewaddress(client: &Client, label: &str, kind: AddressType) -> Result { + const METHOD: &str = "getnewaddress"; + + let addr: Value = client.call(METHOD, &[label.into(), kind.to_string().into()])?; + Ok(Address::from_str(addr.as_str().unwrap()).unwrap()) + } + + pub fn generate_blocks(client: &Client, block_num: u32) -> Result<(), ExplorerError> { + const METHOD: &str = "generatetoaddress"; + + let address = Self::getnewaddress(client, "", AddressType::default())?.to_string(); + client.call::(METHOD, &[block_num.into(), address.into()])?; + Ok(()) + } + + pub fn sweep_initialfreecoins(client: &Client) -> Result<(), ExplorerError> { + const METHOD: &str = "sendtoaddress"; + + let address = Self::getnewaddress(client, "", AddressType::default())?; + client.call::( + METHOD, + &[ + address.to_string().into(), + "21".into(), + "".into(), + "".into(), + true.into(), + ], + )?; + Ok(()) + } + + pub fn issueasset(client: &Client, satoshi: u64) -> Result { + const METHOD: &str = "issueasset"; + + let btc = sat2btc(satoshi); + let r = client.call::(METHOD, &[btc.into(), 0.into()])?; + let asset = r.get("asset").unwrap().as_str().unwrap().to_string(); + Ok(AssetId::from_str(&asset)?) + } + + /// Get the genesis block hash from the running elementsd node. + /// + /// Could differ from the hardcoded one because parameters like `-initialfreecoins` + /// change the genesis hash. + pub fn genesis_block_hash(client: &Client) -> Result { + Self::block_hash(client, 0) + } + + pub fn block_hash(client: &Client, height: u64) -> Result { + const METHOD: &str = "getblockhash"; + + let raw: Value = client.call(METHOD, &[height.into()])?; + Ok(BlockHash::from_str(raw.as_str().unwrap())?) + } + + pub fn getpeginaddress(client: &Client) -> Result<(bitcoin::Address, String), ExplorerError> { + #[derive(serde::Deserialize)] + struct GetpeginaddressResult { + getpeginaddress: String, + claim_script: String, + } + + const METHOD: &str = "getpeginaddress"; + let value: GetpeginaddressResult = client.call(METHOD, &[]).unwrap(); + + let mainchain_address = bitcoin::Address::from_str(&value.getpeginaddress) + .unwrap() + .assume_checked(); + + Ok((mainchain_address, value.claim_script)) + } + + pub fn raw_createpsbt(client: &Client, inputs: Value, outputs: Value) -> Result { + const METHOD: &str = "createpsbt"; + + let psbt: serde_json::Value = client.call(METHOD, &[inputs, outputs, 0.into(), false.into()])?; + Ok(psbt.as_str().unwrap().to_string()) + } + + pub fn expected_next(client: &Client, base64: &str) -> Result { + const METHOD: &str = "analyzepsbt"; + + let value: serde_json::Value = client.call(METHOD, &[base64.into()])?; + Ok(value.get("next").unwrap().as_str().unwrap().to_string()) + } + + pub fn walletprocesspsbt(client: &Client, psbt: &str) -> Result { + const METHOD: &str = "walletprocesspsbt"; + + let value: serde_json::Value = client.call(METHOD, &[psbt.into()])?; + Ok(value.get("psbt").unwrap().as_str().unwrap().to_string()) + } + + pub fn finalizepsbt(client: &Client, psbt: &str) -> Result { + const METHOD: &str = "finalizepsbt"; + + let value: serde_json::Value = client.call(METHOD, &[psbt.into()])?; + assert!(value.get("complete").unwrap().as_bool().unwrap()); + Ok(value.get("hex").unwrap().as_str().unwrap().to_string()) + } + + pub fn sendrawtransaction(client: &Client, tx: &str) -> Result { + const METHOD: &str = "sendrawtransaction"; + + let value: serde_json::Value = client.call(METHOD, &[tx.into()])?; + Ok(value.as_str().unwrap().to_string()) + } + + pub fn testmempoolaccept(client: &Client, tx: &str) -> Result { + const METHOD: &str = "testmempoolaccept"; + + let value: serde_json::Value = client.call(METHOD, &[[tx].into()])?; + Ok(value.as_array().unwrap()[0].get("allowed").unwrap().as_bool().unwrap()) + } + + pub fn create_wallet(client: &Client, wallet_name: Option) -> Result { + const METHOD: &str = "createwallet"; + + #[derive(serde::Deserialize)] + pub struct CreatewalletResult { + name: String, + warning: String, + } + + let value: CreatewalletResult = client.call( + METHOD, + &[ + wallet_name.unwrap_or("my_wallet_name".to_string()).into(), + false.into(), + false.into(), + "".into(), + false.into(), + false.into(), + true.into(), + false.into(), + ], + )?; + Ok(WalletMeta { name: value.name }) + } + + pub fn getbalance(client: &Client, conf: Option) -> Result { + const METHOD: &str = "getbalance"; + + Ok(client.call::(METHOD, &["*".into(), conf.unwrap_or_default().into()])?) + } + + pub fn listunspent( + client: &Client, + min_conf: Option, + max_conf: Option, + addresses: Option>, + include_unsafe: Option, + query_options: Option, + ) -> Result, ExplorerError> { + const METHOD: &str = "listunspent"; + + let mut args = Vec::new(); + args.push(min_conf.unwrap_or(1).into()); + args.push(max_conf.unwrap_or(9999999).into()); + + if let Some(addrs) = addresses { + args.push(addrs.into()); + } else { + args.push(serde_json::to_value(Vec::::new()).unwrap()); + } + + if include_unsafe.is_some() || query_options.is_some() { + args.push(include_unsafe.unwrap_or(true).into()); + } + + if let Some(opts) = query_options { + args.push(serde_json::to_value(opts).unwrap()); + } + + Ok(client.call::>(METHOD, &args)?) + } + + pub fn importaddress( + client: &Client, + address: &str, + label: Option<&str>, + rescan: Option, + p2sh: Option, + ) -> Result<(), ExplorerError> { + const METHOD: &str = "importaddress"; + + let mut args = vec![address.into()]; + + if let Some(lbl) = label { + args.push(lbl.into()); + } else { + args.push("".into()); + } + + if rescan.is_some() || p2sh.is_some() { + args.push(rescan.unwrap_or(true).into()); + } + + if let Some(p2sh_val) = p2sh { + args.push(p2sh_val.into()); + } + + client.call::(METHOD, &args)?; + Ok(()) + } + + pub fn validateaddress(client: &Client, address: &str) -> Result { + const METHOD: &str = "validateaddress"; + + let value: serde_json::Value = client.call(METHOD, &[address.into()])?; + Ok(value + .get("isvalid") + .and_then(|v| v.as_bool()) + .ok_or_else(|| ExplorerError::ElementsRpcUnexpectedReturn(METHOD.into()))?) + } + + pub fn scantxoutset( + client: &Client, + action: &str, + scanobjects: Option>, + ) -> Result { + const METHOD: &str = "scantxoutset"; + + let mut args = vec![action.into()]; + + match action { + "start" => { + if let Some(objects) = scanobjects { + args.push(serde_json::to_value(objects).unwrap()); + } else { + return Err(ExplorerError::InvalidInput( + "scantxoutset 'start' action requires scanobjects".to_string(), + )); + } + } + "abort" | "status" => { + if scanobjects.is_some() { + return Err(ExplorerError::InvalidInput(format!( + "scantxoutset '{}' action does not accept scanobjects", + action + ))); + } + } + _ => { + return Err(ExplorerError::InvalidInput(format!( + "unknown scantxoutset action: {}", + action + ))); + } + } + + let response = client.call::(METHOD, &args)?; + dbg!("response: {}", response.to_string()); + ScantxoutsetResult::from_value(response, action) + .map_err(|e| ExplorerError::ElementsRpcUnexpectedReturn(e.to_string())) + } +} + +fn sat2btc(sat: u64) -> String { + let amount = bitcoin::Amount::from_sat(sat); + amount.to_string_in(bitcoin::amount::Denomination::Bitcoin) +} diff --git a/crates/runtime/src/elements_rpc/types.rs b/crates/runtime/src/elements_rpc/types.rs new file mode 100644 index 0000000..fcb6902 --- /dev/null +++ b/crates/runtime/src/elements_rpc/types.rs @@ -0,0 +1,232 @@ +use serde::ser::Error; +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct GetBlockchainInfo { + pub chain: String, + pub blocks: u64, + pub headers: u64, + pub bestblockhash: String, + // pub difficulty: f64, + pub time: u64, + pub mediantime: u64, + pub verificationprogress: f64, + pub initialblockdownload: bool, + // pub chainwork: String, + pub size_on_disk: u64, + pub pruned: bool, + + // Elements specific fields + pub current_params_root: String, + // pub signblock_asm: String, + // pub signblock_hex: String, + pub current_signblock_asm: String, + pub current_signblock_hex: String, + pub max_block_witness: u64, + pub epoch_length: u64, + pub total_valid_epochs: u64, + pub epoch_age: u64, + + // Using Value here as the documentation describes it generically as "extension fields" + pub extension_space: Vec, + + // Optional pruning fields (only present if pruning is enabled) + #[serde(skip_serializing_if = "Option::is_none")] + pub pruneheight: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub automatic_pruning: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prune_target_size: Option, + + // Softforks are deprecated but might still be present if configured + #[serde(skip_serializing_if = "Option::is_none")] + pub softforks: Option>, + + pub warnings: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct Softfork { + #[serde(rename = "type")] + pub fork_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + pub active: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub bip9: Option, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SoftforkBip9 { + #[serde(skip_serializing_if = "Option::is_none")] + pub bit: Option, + pub start_time: u64, + pub timeout: u64, + pub min_activation_height: u64, + pub status: String, + pub since: u64, + pub status_next: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub statistics: Option, + pub signalling: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SoftforkStatistics { + pub period: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub threshold: Option, + pub elapsed: u64, + pub count: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub possible: Option, +} + +pub struct WalletMeta { + pub name: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct GetBalance { + #[serde(rename = "mine")] + pub mine: BalanceDetails, + #[serde(rename = "watchonly")] + pub watchonly: Option, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct BalanceDetails { + pub trusted: f64, + pub untrusted_pending: f64, + pub immature: f64, +} + +#[derive(Default, Debug, Clone, Copy)] +pub enum AddressType { + Legacy, + #[default] + P2shSegwit, + Bech32, + Bech32m, +} + +impl std::fmt::Display for AddressType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + AddressType::Legacy => "legacy".to_string(), + AddressType::P2shSegwit => "p2sh-segwit".to_string(), + AddressType::Bech32 => "bech32".to_string(), + AddressType::Bech32m => "bech32m".to_string(), + }; + write!(f, "{}", str) + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ListUnspent { + pub txid: String, + pub vout: u32, + pub address: String, + pub label: Option, + #[serde(rename = "scriptPubKey")] + pub script_pubkey: String, + pub amount: f64, + pub amountcommitment: Option, + pub asset: Option, + pub assetcommitment: Option, + pub confirmations: u64, + pub bcconfirmations: Option, + pub spendable: bool, + pub solvable: bool, + pub safe: bool, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct QueryOptions { + #[serde(rename = "minimumAmount")] + pub minimum_amount: Option, + #[serde(rename = "maximumAmount")] + pub maximum_amount: Option, + #[serde(rename = "maximumCount")] + pub maximum_count: Option, + #[serde(rename = "minimumSumAmount")] + pub minimum_sum_amount: Option, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum ScantxoutsetResult { + Start { + bestblock: String, + height: u64, + success: bool, + total_unblinded_bitcoin_amount: f64, + txouts: u32, + unspents: Vec, + }, + Abort { + success: bool, + }, + Status { + progress: f64, + searched_items: Option, + }, +} + +impl ScantxoutsetResult { + /// Parse the RPC response based on the action that was sent + pub fn from_value(value: serde_json::Value, action: &str) -> Result { + match action { + "start" => serde_json::from_value(value).map(|start_data: StartData| ScantxoutsetResult::Start { + bestblock: start_data.bestblock, + height: start_data.height, + success: start_data.success, + total_unblinded_bitcoin_amount: start_data.total_unblinded_bitcoin_amount, + txouts: start_data.txouts, + unspents: start_data.unspents, + }), + "abort" => serde_json::from_value(value).map(|abort_data: AbortData| ScantxoutsetResult::Abort { + success: abort_data.success, + }), + "status" => serde_json::from_value(value).map(|status_data: StatusData| ScantxoutsetResult::Status { + progress: status_data.progress, + searched_items: status_data.searched_items, + }), + _ => Err(serde_json::Error::custom(format!("unknown action: {}", action))), + } + } +} + +#[derive(Debug, serde::Deserialize)] +struct StartData { + bestblock: String, + height: u64, + success: bool, + total_unblinded_bitcoin_amount: f64, + txouts: u32, + unspents: Vec, +} + +#[derive(Debug, serde::Deserialize)] +struct AbortData { + success: bool, +} + +#[derive(Debug, serde::Deserialize)] +struct StatusData { + progress: f64, + searched_items: Option, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ScantxoutsetUtxo { + pub amount: f64, + pub asset: String, + pub desc: String, + pub height: u64, + #[serde(rename = "scriptPubKey")] + pub scriptpubkey: String, + pub txid: String, + pub vout: u32, +} diff --git a/crates/explorer/src/error.rs b/crates/runtime/src/error.rs similarity index 83% rename from crates/explorer/src/error.rs rename to crates/runtime/src/error.rs index a8d7ab2..2b58259 100644 --- a/crates/explorer/src/error.rs +++ b/crates/runtime/src/error.rs @@ -1,14 +1,10 @@ -use reqwest::StatusCode; -use url::Url; +use reqwest::{StatusCode, Url}; -#[derive(thiserror::Error, Debug, Clone)] +#[derive(thiserror::Error, Debug)] pub enum ExplorerError { #[error("Failed to type to Url, {0}")] UrlConversion(String), - #[error("url")] - UrlParsing(#[from] url::ParseError), - #[error("Failed to send request, [url: '{url:?}', code: {status:?}, text: '{text}']")] Request { url: Option, @@ -42,14 +38,23 @@ pub enum ExplorerError { #[error("Failed to decode commitment, type: {commitment_type:?}, error: {error}")] CommitmentDecode { commitment_type: CommitmentType, - error: String, + error: simplicityhl::elements::encode::Error, }, #[error("Failed to decode hex string using hex_simd, error: {0}")] - HexSimdDecode(String), + HexSimdDecode(hex_simd::Error), #[error("Failed to deserialize Transaction from hex, error: {0}")] TransactionDecode(String), + + #[error(transparent)] + ElementsRpcError(#[from] electrsd::bitcoind::bitcoincore_rpc::Error), + + #[error("Elements RPC returned an unexpected value for call {0}")] + ElementsRpcUnexpectedReturn(String), + + #[error("Invalid input, err: {0}")] + InvalidInput(String), } #[derive(Debug, Clone)] diff --git a/crates/explorer/src/esplora/mod.rs b/crates/runtime/src/esplora/mod.rs similarity index 98% rename from crates/explorer/src/esplora/mod.rs rename to crates/runtime/src/esplora/mod.rs index 8db8a26..74bc175 100644 --- a/crates/explorer/src/esplora/mod.rs +++ b/crates/runtime/src/esplora/mod.rs @@ -267,28 +267,25 @@ mod deserializable { valuecommitment, } => types::UtxoInfo::Confidential { asset_comm: Asset::from_commitment( - &hex_simd::decode_to_vec(assetcommitment) - .map_err(|e| ExplorerError::HexSimdDecode(e.to_string()))?, + &hex_simd::decode_to_vec(assetcommitment).map_err(ExplorerError::HexSimdDecode)?, ) .map_err(|e| ExplorerError::CommitmentDecode { commitment_type: CommitmentType::Asset, - error: e.to_string(), + error: e, })?, value_comm: Value::from_commitment( - &hex_simd::decode_to_vec(valuecommitment) - .map_err(|e| ExplorerError::HexSimdDecode(e.to_string()))?, + &hex_simd::decode_to_vec(valuecommitment).map_err(ExplorerError::HexSimdDecode)?, ) .map_err(|e| ExplorerError::CommitmentDecode { commitment_type: CommitmentType::Asset, - error: e.to_string(), + error: e, })?, nonce_comm: Nonce::from_commitment( - &hex_simd::decode_to_vec(noncecommitment) - .map_err(|e| ExplorerError::HexSimdDecode(e.to_string()))?, + &hex_simd::decode_to_vec(noncecommitment).map_err(ExplorerError::HexSimdDecode)?, ) .map_err(|e| ExplorerError::CommitmentDecode { commitment_type: CommitmentType::Asset, - error: e.to_string(), + error: e, })?, }, UtxoInfo::Explicit { asset, value } => types::UtxoInfo::Explicit { diff --git a/crates/explorer/src/esplora/types.rs b/crates/runtime/src/esplora/types.rs similarity index 100% rename from crates/explorer/src/esplora/types.rs rename to crates/runtime/src/esplora/types.rs diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs new file mode 100644 index 0000000..a58835d --- /dev/null +++ b/crates/runtime/src/lib.rs @@ -0,0 +1,7 @@ +pub mod elements_rpc; +mod error; +pub mod esplora; +mod waterfall; + +pub use error::*; +// pub use waterfall::*; diff --git a/crates/explorer/src/waterfall/mod.rs b/crates/runtime/src/waterfall/mod.rs similarity index 100% rename from crates/explorer/src/waterfall/mod.rs rename to crates/runtime/src/waterfall/mod.rs diff --git a/crates/explorer/src/waterfall/types.rs b/crates/runtime/src/waterfall/types.rs similarity index 100% rename from crates/explorer/src/waterfall/types.rs rename to crates/runtime/src/waterfall/types.rs diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml new file mode 100644 index 0000000..1048084 --- /dev/null +++ b/crates/sdk/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "simplex-sdk" +version = "0.1.0" +edition = "2024" +description = "Simplex SDK" +license = "MIT OR Apache-2.0" +readme = "README.md" + +[lints] +workspace = true + +[dependencies] +simplex-runtime = { workspace = true } + +thiserror = { workspace = true } +sha2 = { workspace = true } +minreq = { workspace = true } +simplicityhl = { workspace = true } \ No newline at end of file diff --git a/crates/sdk/src/arguments.rs b/crates/sdk/src/arguments.rs new file mode 100644 index 0000000..c2ca884 --- /dev/null +++ b/crates/sdk/src/arguments.rs @@ -0,0 +1,5 @@ +use simplicityhl::Arguments; + +pub trait ArgumentsTrait { + fn build_arguments(&self) -> Arguments; +} diff --git a/crates/sdk/src/constants.rs b/crates/sdk/src/constants.rs new file mode 100644 index 0000000..b692d5b --- /dev/null +++ b/crates/sdk/src/constants.rs @@ -0,0 +1,89 @@ +use simplicityhl::simplicity::elements; +use simplicityhl::simplicity::hashes::{Hash, sha256}; + +use std::str::FromStr; + +pub const PUBLIC_SECRET_BLINDER_KEY: [u8; 32] = [1; 32]; + +pub const DEFAULT_TARGET_BLOCKS: u32 = 0; +pub const DEFAULT_FEE_RATE: f32 = 100.0; +pub const WITNESS_SCALE_FACTOR: usize = 4; +pub const PLACEHOLDER_FEE: u64 = 1; + +/// Policy asset id (hex, BE) for Liquid mainnet. +pub const LIQUID_POLICY_ASSET_STR: &str = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"; + +/// Policy asset id (hex, BE) for Liquid testnet. +pub const LIQUID_TESTNET_POLICY_ASSET_STR: &str = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; + +/// Policy asset id (hex, BE) for Elements regtest. +pub const LIQUID_DEFAULT_REGTEST_ASSET_STR: &str = "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"; + +/// Example test asset id (hex, BE) on Liquid testnet. +pub static LIQUID_TESTNET_TEST_ASSET_ID_STR: &str = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; + +pub static LIQUID_TESTNET_BITCOIN_ASSET: std::sync::LazyLock = std::sync::LazyLock::new(|| { + elements::AssetId::from_inner(sha256::Midstate([ + 0x49, 0x9a, 0x81, 0x85, 0x45, 0xf6, 0xba, 0xe3, 0x9f, 0xc0, 0x3b, 0x63, 0x7f, 0x2a, 0x4e, 0x1e, 0x64, 0xe5, + 0x90, 0xca, 0xc1, 0xbc, 0x3a, 0x6f, 0x6d, 0x71, 0xaa, 0x44, 0x43, 0x65, 0x4c, 0x14, + ])) +}); + +pub static LIQUID_MAINNET_GENESIS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + elements::BlockHash::from_byte_array([ + 0x03, 0x60, 0x20, 0x8a, 0x88, 0x96, 0x92, 0x37, 0x2c, 0x8d, 0x68, 0xb0, 0x84, 0xa6, 0x2e, 0xfd, 0xf6, 0x0e, + 0xa1, 0xa3, 0x59, 0xa0, 0x4c, 0x94, 0xb2, 0x0d, 0x22, 0x36, 0x58, 0x27, 0x66, 0x14, + ]) +}); + +pub static LIQUID_TESTNET_GENESIS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + elements::BlockHash::from_byte_array([ + 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, 0x79, 0x3b, 0x5b, 0x5e, + 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52, 0x8e, 0xda, 0x71, 0xa7, + ]) +}); + +pub static LIQUID_REGTEST_GENESIS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + elements::BlockHash::from_byte_array([ + 0x21, 0xca, 0xb1, 0xe5, 0xda, 0x47, 0x18, 0xea, 0x14, 0x0d, 0x97, 0x16, 0x93, 0x17, 0x02, 0x42, 0x2f, 0x0e, + 0x6a, 0xd9, 0x15, 0xc8, 0xd9, 0xb5, 0x83, 0xca, 0xc2, 0x70, 0x6b, 0x2a, 0x90, 0x00, + ]) +}); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SimplicityNetwork { + Liquid, + LiquidTestnet, + ElementsRegtest { policy_asset: elements::AssetId }, +} + +impl SimplicityNetwork { + pub fn default_regtest() -> Self { + let policy_asset = elements::AssetId::from_str(LIQUID_DEFAULT_REGTEST_ASSET_STR).unwrap(); + Self::ElementsRegtest { policy_asset } + } + + pub fn policy_asset(&self) -> elements::AssetId { + match self { + Self::Liquid => elements::AssetId::from_str(LIQUID_POLICY_ASSET_STR).unwrap(), + Self::LiquidTestnet => elements::AssetId::from_str(LIQUID_TESTNET_POLICY_ASSET_STR).unwrap(), + Self::ElementsRegtest { policy_asset } => *policy_asset, + } + } + + pub fn genesis_block_hash(&self) -> elements::BlockHash { + match self { + Self::Liquid => *LIQUID_MAINNET_GENESIS, + Self::LiquidTestnet => *LIQUID_TESTNET_GENESIS, + Self::ElementsRegtest { .. } => *LIQUID_REGTEST_GENESIS, + } + } + + pub const fn address_params(&self) -> &'static elements::AddressParams { + match self { + Self::Liquid => &elements::AddressParams::LIQUID, + Self::LiquidTestnet => &elements::AddressParams::LIQUID_TESTNET, + Self::ElementsRegtest { .. } => &elements::AddressParams::ELEMENTS, + } + } +} diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs new file mode 100644 index 0000000..dafe435 --- /dev/null +++ b/crates/sdk/src/error.rs @@ -0,0 +1,46 @@ +use simplicityhl::elements::secp256k1_zkp; + +#[derive(Debug, thiserror::Error)] +pub enum SimplexError { + #[error("Failed to compile Simplicity program: {0}")] + Compilation(String), + + #[error("Failed to satisfy witness: {0}")] + WitnessSatisfaction(String), + + #[error("Failed to prune program: {0}")] + Pruning(#[from] simplicityhl::simplicity::bit_machine::ExecutionError), + + #[error("Failed to construct a Bit Machine with enough space: {0}")] + BitMachineCreation(#[from] simplicityhl::simplicity::bit_machine::LimitError), + + #[error("Failed to execute program on the Bit Machine: {0}")] + Execution(simplicityhl::simplicity::bit_machine::ExecutionError), + + #[error("UTXO index {input_index} out of bounds (have {utxo_count} UTXOs)")] + UtxoIndexOutOfBounds { input_index: usize, utxo_count: usize }, + + #[error("Script pubkey mismatch: expected hash {expected_hash}, got {actual_hash}")] + ScriptPubkeyMismatch { expected_hash: String, actual_hash: String }, + + #[error("Input index exceeds u32 maximum: {0}")] + InputIndexOverflow(#[from] std::num::TryFromIntError), + + #[error("Invalid seed length: expected 32 bytes, got {0}")] + InvalidSeedLength(usize), + + #[error("Invalid secret key")] + InvalidSecretKey(#[from] secp256k1_zkp::UpstreamError), + + #[error("HTTP request failed: {0}")] + Request(String), + + #[error("Broadcast failed with HTTP {status} for {url}: {message}")] + BroadcastRejected { status: u16, url: String, message: String }, + + #[error("Failed to deserialize response: {0}")] + Deserialize(String), + + #[error("Invalid txid format: {0}")] + InvalidTxid(String), +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs new file mode 100644 index 0000000..fe7733a --- /dev/null +++ b/crates/sdk/src/lib.rs @@ -0,0 +1,9 @@ +pub mod arguments; +pub mod constants; +pub mod error; +pub mod program; +pub mod provider; +pub mod signed_transaction; +pub mod signer; +pub mod utils; +pub mod witness; diff --git a/crates/sdk/src/program.rs b/crates/sdk/src/program.rs new file mode 100644 index 0000000..14e7cc9 --- /dev/null +++ b/crates/sdk/src/program.rs @@ -0,0 +1,217 @@ +use std::sync::Arc; + +use sha2::{Digest, Sha256}; + +use simplicityhl::CompiledProgram; +use simplicityhl::WitnessValues; +use simplicityhl::elements::{Address, Script, Transaction, TxInWitness, TxOut, script, taproot}; +use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; +use simplicityhl::simplicity::jet::Elements; +use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; +use simplicityhl::simplicity::{BitMachine, RedeemNode, Value}; +use simplicityhl::tracker::{DefaultTracker, TrackerLogLevel}; + +use crate::arguments::ArgumentsTrait; +use crate::constants::SimplicityNetwork; +use crate::error::SimplexError; + +pub trait ProgramTrait { + fn get_env( + &self, + tx: &Transaction, + utxos: &[TxOut], + input_index: usize, + network: SimplicityNetwork, + ) -> Result>, SimplexError>; + + fn execute( + &self, + witness: WitnessValues, + tx: &Transaction, + utxos: &[TxOut], + input_index: usize, + network: SimplicityNetwork, + ) -> Result<(Arc>, Value), SimplexError>; + + fn finalize( + &self, + witness: WitnessValues, + tx: Transaction, + utxos: &[TxOut], + input_index: usize, + network: SimplicityNetwork, + ) -> Result; +} + +pub struct Program<'a> { + source: &'static str, + pub_key: &'a XOnlyPublicKey, + arguments: &'a dyn ArgumentsTrait, +} + +impl<'a> ProgramTrait for Program<'a> { + fn get_env( + &self, + tx: &Transaction, + utxos: &[TxOut], + input_index: usize, + network: SimplicityNetwork, + ) -> Result>, SimplexError> { + let genesis_hash = network.genesis_block_hash(); + let cmr = self.load()?.commit().cmr(); + + if utxos.len() <= input_index { + return Err(SimplexError::UtxoIndexOutOfBounds { + input_index, + utxo_count: utxos.len(), + }); + } + + let target_utxo = &utxos[input_index]; + let script_pubkey = self.get_tr_address(network)?.script_pubkey(); + + if target_utxo.script_pubkey != script_pubkey { + return Err(SimplexError::ScriptPubkeyMismatch { + expected_hash: script_pubkey.script_hash().to_string(), + actual_hash: target_utxo.script_pubkey.script_hash().to_string(), + }); + } + + Ok(ElementsEnv::new( + Arc::new(tx.clone()), + utxos + .iter() + .map(|utxo| ElementsUtxo { + script_pubkey: utxo.script_pubkey.clone(), + asset: utxo.asset, + value: utxo.value, + }) + .collect(), + u32::try_from(input_index)?, + cmr, + self.control_block()?, + None, + genesis_hash, + )) + } + + fn execute( + &self, + witness: WitnessValues, + tx: &Transaction, + utxos: &[TxOut], + input_index: usize, + network: SimplicityNetwork, + ) -> Result<(Arc>, Value), SimplexError> { + let satisfied = self + .load()? + .satisfy(witness) + .map_err(SimplexError::WitnessSatisfaction)?; + + let mut tracker = DefaultTracker::new(satisfied.debug_symbols()).with_log_level(TrackerLogLevel::Debug); + + let env = self.get_env(tx, utxos, input_index, network)?; + + let pruned = satisfied.redeem().prune_with_tracker(&env, &mut tracker)?; + let mut mac = BitMachine::for_program(&pruned)?; + + let result = mac.exec(&pruned, &env).map_err(SimplexError::Execution)?; + + Ok((pruned, result)) + } + + fn finalize( + &self, + witness: WitnessValues, + mut tx: Transaction, + utxos: &[TxOut], + input_index: usize, + network: SimplicityNetwork, + ) -> Result { + let pruned = self.execute(witness, &tx, utxos, input_index, network)?.0; + + let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); + let cmr = pruned.cmr(); + + tx.input[input_index].witness = TxInWitness { + amount_rangeproof: None, + inflation_keys_rangeproof: None, + script_witness: vec![ + simplicity_witness_bytes, + simplicity_program_bytes, + cmr.as_ref().to_vec(), + self.control_block()?.serialize(), + ], + pegin_witness: vec![], + }; + + Ok(tx) + } +} + +impl<'a> Program<'a> { + pub fn new(source: &'static str, pub_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { + Self { + source: source, + pub_key: pub_key, + arguments: arguments, + } + } + + pub fn get_tr_address(&self, network: SimplicityNetwork) -> Result { + let spend_info = self.taproot_spending_info()?; + + Ok(Address::p2tr( + secp256k1::SECP256K1, + spend_info.internal_key(), + spend_info.merkle_root(), + None, + network.address_params(), + )) + } + + pub fn get_script_pubkey(&self, network: SimplicityNetwork) -> Result { + Ok(self.get_tr_address(network)?.script_pubkey()) + } + + pub fn get_script_hash(&self, network: SimplicityNetwork) -> Result<[u8; 32], SimplexError> { + let script = self.get_script_pubkey(network)?; + let mut hasher = Sha256::new(); + + sha2::digest::Update::update(&mut hasher, script.as_bytes()); + Ok(hasher.finalize().into()) + } + + fn load(&self) -> Result { + let compiled = CompiledProgram::new(self.source, self.arguments.build_arguments(), true) + .map_err(SimplexError::Compilation)?; + Ok(compiled) + } + + fn script_version(&self) -> Result<(Script, taproot::LeafVersion), SimplexError> { + let cmr = self.load()?.commit().cmr(); + let script = script::Script::from(cmr.as_ref().to_vec()); + + Ok((script, simplicityhl::simplicity::leaf_version())) + } + + fn taproot_spending_info(&self) -> Result { + let builder = taproot::TaprootBuilder::new(); + let (script, version) = self.script_version()?; + + let builder = builder + .add_leaf_with_ver(0, script, version) + .expect("tap tree should be valid"); + + Ok(builder + .finalize(secp256k1::SECP256K1, *self.pub_key) + .expect("tap tree should be valid")) + } + + fn control_block(&self) -> Result { + let info = self.taproot_spending_info()?; + let script_ver = self.script_version()?; + + Ok(info.control_block(&script_ver).expect("control block should exist")) + } +} diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs new file mode 100644 index 0000000..a4927ab --- /dev/null +++ b/crates/sdk/src/provider/esplora.rs @@ -0,0 +1,20 @@ +use crate::error::SimplexError; +use crate::provider::Provider; +use simplicityhl::elements::{Transaction, Txid}; +use std::collections::HashMap; + +pub use simplex_runtime::esplora::*; + +impl Provider for EsploraClient { + fn broadcast_transaction(&self, tx: &Transaction) -> Result { + todo!() + } + + fn fetch_fee_estimates(&self) -> Result, SimplexError> { + todo!() + } + + fn fetch_transaction(&self, txid: Txid) -> Result { + todo!() + } +} diff --git a/crates/sdk/src/provider/mod.rs b/crates/sdk/src/provider/mod.rs new file mode 100644 index 0000000..3d6a3a0 --- /dev/null +++ b/crates/sdk/src/provider/mod.rs @@ -0,0 +1,127 @@ +mod esplora; + +use std::collections::HashMap; + +use simplicityhl::elements::encode; +use simplicityhl::elements::hex::ToHex; +use simplicityhl::elements::{Transaction, Txid}; + +use crate::constants::DEFAULT_FEE_RATE; +use crate::error::SimplexError; + +pub trait Provider { + fn broadcast_transaction(&self, tx: &Transaction) -> Result; + + fn fetch_fee_estimates(&self) -> Result, SimplexError>; + + fn fetch_transaction(&self, txid: Txid) -> Result; + + fn get_fee_rate(&self, target_blocks: u32) -> Result { + if target_blocks == 0 { + return Ok(DEFAULT_FEE_RATE); + } + + let estimates = self.fetch_fee_estimates()?; + + let target_str = target_blocks.to_string(); + + if let Some(&rate) = estimates.get(&target_str) { + return Ok((rate * 1000.0) as f32); // Convert sat/vB to sats/kvb + } + + let fallback_targets = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 144, 504, 1008, + ]; + + for &target in fallback_targets.iter().filter(|&&t| t >= target_blocks) { + let key = target.to_string(); + + if let Some(&rate) = estimates.get(&key) { + return Ok((rate * 1000.0) as f32); + } + } + + for &target in &fallback_targets { + let key = target.to_string(); + + if let Some(&rate) = estimates.get(&key) { + return Ok((rate * 1000.0) as f32); + } + } + + Err(SimplexError::Request("No fee estimates available".to_string())) + } +} + +pub struct EsploraProvider { + esplora_url: String, +} + +impl EsploraProvider { + pub fn new(url: String) -> Self { + Self { esplora_url: url } + } +} + +impl Provider for EsploraProvider { + fn broadcast_transaction(&self, tx: &Transaction) -> Result { + let tx_hex = encode::serialize_hex(tx); + let url = format!("{}/tx", self.esplora_url); + + let response = minreq::post(&url) + .with_body(tx_hex) + .send() + .map_err(|e| SimplexError::Request(e.to_string()))?; + + let status = response.status_code; + let body = response.as_str().unwrap_or("").trim().to_owned(); + + if !(200..300).contains(&status) { + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + return Err(SimplexError::BroadcastRejected { + status: status as u16, + url: format!("{}/tx", self.esplora_url), + message: body, + }); + } + + Ok(body) + } + + fn fetch_fee_estimates(&self) -> Result, SimplexError> { + let url = self.esplora_url.clone() + "/fee-estimates"; + let response = minreq::get(&url) + .send() + .map_err(|e| SimplexError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(SimplexError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let estimates: HashMap = response.json().map_err(|e| SimplexError::Deserialize(e.to_string()))?; + + Ok(estimates) + } + + fn fetch_transaction(&self, txid: Txid) -> Result { + let url = self.esplora_url.clone() + "/tx/" + txid.to_hex().as_str() + "/raw"; + let response = minreq::get(&url) + .send() + .map_err(|e| SimplexError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(SimplexError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let bytes = response.as_bytes(); + let tx: Transaction = encode::deserialize(bytes).map_err(|e| SimplexError::Deserialize(e.to_string()))?; + + Ok(tx) + } +} diff --git a/crates/sdk/src/signed_transaction.rs b/crates/sdk/src/signed_transaction.rs new file mode 100644 index 0000000..3e71102 --- /dev/null +++ b/crates/sdk/src/signed_transaction.rs @@ -0,0 +1,135 @@ +use simplicityhl::WitnessValues; +use simplicityhl::elements::secp256k1_zkp::schnorr::Signature; +use simplicityhl::elements::{Transaction, TxOut}; + +use crate::constants::{SimplicityNetwork, WITNESS_SCALE_FACTOR}; +use crate::error::SimplexError; +use crate::program::ProgramTrait; +use crate::provider::Provider; +use crate::signer::SignerTrait; +use crate::witness::WitnessTrait; + +struct SignedInput<'a, T> { + program: &'a dyn ProgramTrait, + witness: &'a dyn WitnessTrait, + signer: Option<&'a dyn SignerTrait>, + signer_lambda: Option, +} + +pub struct SignedTransaction<'a, T> { + tx: Transaction, + utxos: &'a [TxOut], + network: SimplicityNetwork, + inputs: Vec>, +} + +impl<'a, T> SignedTransaction<'a, T> +where + T: Fn(WitnessValues, Signature) -> Result + Clone, +{ + pub fn new(tx: Transaction, utxos: &'a [TxOut], network: SimplicityNetwork) -> Self { + Self { + tx, + utxos, + network, + inputs: Vec::new(), + } + } + + pub fn add_input(&mut self, program: &'a dyn ProgramTrait, witness: &'a dyn WitnessTrait) { + let signed_input = SignedInput { + program, + witness, + signer: None, + signer_lambda: None, + }; + + self.inputs.push(signed_input); + } + + pub fn add_signed_input( + &mut self, + program: &'a dyn ProgramTrait, + witness: &'a dyn WitnessTrait, + signer: &'a dyn SignerTrait, + signer_lambda: T, + ) { + let signed_input = SignedInput { + program, + witness, + signer: Some(signer), + signer_lambda: Some(signer_lambda), + }; + + self.inputs.push(signed_input); + } + + pub fn finalize_with_fee( + &self, + target_blocks: u32, + provider: impl Provider, + ) -> Result<(Transaction, u64), SimplexError> { + let fee_rate = provider.get_fee_rate(target_blocks)?; + let final_tx = self.finalize()?; + + let fee = self.calculate_fee(final_tx.weight(), fee_rate); + + Ok((final_tx, fee)) + } + + pub fn finalize(&self) -> Result { + let mut final_tx = self.tx.clone(); + + for index in 0..self.inputs.len() { + let (program, witness, signer, signer_lambda) = { + let input = &self.inputs[index]; + (input.program, input.witness, input.signer, input.signer_lambda.clone()) + }; + + if signer.is_some() { + final_tx = self.finalize_with_signer( + final_tx, + program, + witness.build_witness(), + index, + signer.unwrap(), + signer_lambda.unwrap(), + )?; + } else { + final_tx = self.finalize_as_is(final_tx, program, witness.build_witness(), index)?; + } + } + + Ok(final_tx) + } + + fn finalize_with_signer( + &self, + final_tx: Transaction, + program: &dyn ProgramTrait, + witness: WitnessValues, + index: usize, + signer: &dyn SignerTrait, + signer_lambda: T, + ) -> Result { + let signature = signer.sign(program, &final_tx, self.utxos, index, self.network)?; + let new_witness = signer_lambda(witness, signature)?; + + Ok(self.finalize_as_is(final_tx, program, new_witness, index)?) + } + + fn finalize_as_is( + &self, + final_tx: Transaction, + program: &dyn ProgramTrait, + witness: WitnessValues, + index: usize, + ) -> Result { + Ok(program.finalize(witness, final_tx, self.utxos, index, self.network)?) + } + + fn calculate_fee(&self, weight: usize, fee_rate: f32) -> u64 { + let vsize = weight.div_ceil(WITNESS_SCALE_FACTOR); + (vsize as f32 * fee_rate / 1000.0).ceil() as u64 + } +} diff --git a/crates/sdk/src/signer.rs b/crates/sdk/src/signer.rs new file mode 100644 index 0000000..fa3002a --- /dev/null +++ b/crates/sdk/src/signer.rs @@ -0,0 +1,66 @@ +use simplicityhl::elements::secp256k1_zkp::{self as secp256k1, Keypair, Message, schnorr::Signature}; +use simplicityhl::elements::{Transaction, TxOut}; +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; +use simplicityhl::simplicity::hashes::Hash as _; + +use crate::constants::SimplicityNetwork; +use crate::error::SimplexError; +use crate::program::ProgramTrait; + +pub trait SignerTrait { + fn public_key(&self) -> XOnlyPublicKey; + + fn personal_sign(&self, message: Message) -> Result; + + fn sign<'a>( + &self, + program: &dyn ProgramTrait, + tx: &Transaction, + utxos: &[TxOut], + input_index: usize, + network: SimplicityNetwork, + ) -> Result; +} + +pub struct Signer { + keypair: Keypair, +} + +impl SignerTrait for Signer { + fn public_key(&self) -> XOnlyPublicKey { + self.keypair.x_only_public_key().0 + } + + fn personal_sign(&self, message: Message) -> Result { + Ok(self.keypair.sign_schnorr(message)) + } + + fn sign<'a>( + &self, + program: &dyn ProgramTrait, + tx: &Transaction, + utxos: &[TxOut], + input_index: usize, + network: SimplicityNetwork, + ) -> Result { + let env = program.get_env(tx, utxos, input_index, network)?; + + let sighash_all = Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); + + Ok(self.keypair.sign_schnorr(sighash_all)) + } +} + +impl Signer { + pub const SEED_LEN: usize = secp256k1::constants::SECRET_KEY_SIZE; + + pub fn from_seed(seed: &[u8; Self::SEED_LEN]) -> Result { + let secp = secp256k1::Secp256k1::new(); + + let secret_key = secp256k1::SecretKey::from_slice(seed)?; + + let keypair = Keypair::from_secret_key(&secp, &secret_key); + + Ok(Self { keypair }) + } +} diff --git a/crates/sdk/src/utils.rs b/crates/sdk/src/utils.rs new file mode 100644 index 0000000..d1577c8 --- /dev/null +++ b/crates/sdk/src/utils.rs @@ -0,0 +1,9 @@ +use simplicityhl::simplicity::bitcoin::secp256k1; + +pub fn tr_unspendable_key() -> secp256k1::XOnlyPublicKey { + secp256k1::XOnlyPublicKey::from_slice(&[ + 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, 0x07, 0x8a, + 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, + ]) + .expect("key should be valid") +} diff --git a/crates/sdk/src/witness.rs b/crates/sdk/src/witness.rs new file mode 100644 index 0000000..526c48e --- /dev/null +++ b/crates/sdk/src/witness.rs @@ -0,0 +1,5 @@ +use simplicityhl::WitnessValues; + +pub trait WitnessTrait { + fn build_witness(&self) -> WitnessValues; +} diff --git a/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml index e185769..1abd039 100644 --- a/crates/simplex/Cargo.toml +++ b/crates/simplex/Cargo.toml @@ -21,13 +21,18 @@ macros = ["dep:simplex-macros"] core = ["dep:simplex-core"] [dependencies] -simplex-macros = { path = "../macros", features = [], optional = true } -simplex-core = { path = "../core", features = ["encoding"], optional = true } +simplex-macros = { workspace = true, features = [], optional = true } +simplex-core = { workspace = true, features = ["encoding"], optional = true } +simplex-test = { workspace = true } +simplex-runtime = { workspace = true } bincode = { workspace = true, optional = true } simplicityhl = { workspace = true, features = ["serde"] } serde = { version = "1.0.228" } either = { version = "1.15.0", features = ["serde"] } +tokio = { version = "1.49.0", features = ["full"]} + [dev-dependencies] trybuild = { version = "1.0.115" } +anyhow = { version = "1.0.101" } diff --git a/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs index 249cb3f..56fd2f4 100644 --- a/crates/simplex/src/lib.rs +++ b/crates/simplex/src/lib.rs @@ -10,3 +10,6 @@ pub extern crate simplex_macros; #[cfg(feature = "core")] pub extern crate simplex_core; + +#[cfg(feature = "macros")] +pub extern crate simplex_test; diff --git a/crates/simplex/tests/simplex_test.rs b/crates/simplex/tests/simplex_test.rs new file mode 100644 index 0000000..475fdc4 --- /dev/null +++ b/crates/simplex/tests/simplex_test.rs @@ -0,0 +1,101 @@ +use simplex_runtime::elements_rpc::{AddressType, ElementsRpcClient}; +use simplex_test::DEFAULT_SAT_AMOUNT_FAUCET; +use simplicityhl::elements::Address; +use simplicityhl::elements::bitcoin::secp256k1; +use simplicityhl::elements::secp256k1_zkp::Keypair; + +#[simplex::simplex_macros::test] +// #[test] +fn test_execution() { + assert!(true); +} + +#[test] +fn test_invocation_tx_tracking() -> anyhow::Result<()> { + use simplex_test::{ConfigOption, TestProvider}; + + fn test_invocation_tx_tracking(rpc: TestProvider, user1_addr: Address, user2_addr: Address) -> anyhow::Result<()> { + // user input code + { + let network = rpc.network(); + let keypair = Keypair::from_seckey_slice(&secp256k1::SECP256K1, &[1; 32])?; + let p2pk = simplex_core::get_p2pk_address(&keypair.x_only_public_key().0, network)?; + + dbg!(p2pk.to_string()); + + // simplex runtime + // - test provider + // - fields from config + // - + // p2tr + + // TODO: uncomment and fix + dbg!(ElementsRpcClient::validateaddress(rpc.as_ref(), &p2pk.to_string())?); + // ElementsRpcClient::importaddress(rpc.as_ref(), &p2pk.to_string(), None, None, None)?; + + // broadcast, fetch fee transaction + + ElementsRpcClient::sendtoaddress( + rpc.as_ref(), + &p2pk, + DEFAULT_SAT_AMOUNT_FAUCET, + Some(rpc.network().policy_asset()), + )?; + + ElementsRpcClient::generate_blocks(rpc.as_ref(), 5)?; + + dbg!(ElementsRpcClient::listunspent( + rpc.as_ref(), + None, + None, + Some(vec![p2pk.to_string()]), + None, + None, + )?,); + + dbg!(ElementsRpcClient::scantxoutset( + rpc.as_ref(), + "start", + Some(vec![format!("addr({})", p2pk)]), + )?,); + + Ok(()) + } + } + let rpc = TestProvider::init(ConfigOption::DefaultRegtest).unwrap(); + { + ElementsRpcClient::generate_blocks(rpc.as_ref(), 1).unwrap(); + ElementsRpcClient::rescanblockchain(rpc.as_ref(), None, None).unwrap(); + ElementsRpcClient::sweep_initialfreecoins(rpc.as_ref()).unwrap(); + ElementsRpcClient::generate_blocks(rpc.as_ref(), 100).unwrap(); + } + + let user1_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); + let user2_addr = ElementsRpcClient::getnewaddress(rpc.as_ref(), "", AddressType::default()).unwrap(); + ElementsRpcClient::sendtoaddress( + rpc.as_ref(), + &user1_addr, + DEFAULT_SAT_AMOUNT_FAUCET, + Some(rpc.network().policy_asset()), + ) + .unwrap(); + + ElementsRpcClient::sendtoaddress( + rpc.as_ref(), + &user2_addr, + DEFAULT_SAT_AMOUNT_FAUCET, + Some(rpc.network().policy_asset()), + ) + .unwrap(); + + ElementsRpcClient::generate_blocks(rpc.as_ref(), 3).unwrap(); + dbg!(ElementsRpcClient::listunspent( + rpc.as_ref(), + None, + None, + Some(vec![user1_addr.to_string(), user2_addr.to_string()]), + None, + None, + )?,); + test_invocation_tx_tracking(rpc, user1_addr, user2_addr) +} diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml new file mode 100644 index 0000000..8399bfe --- /dev/null +++ b/crates/test/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "simplex-test" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + + +[dependencies] +simplex-runtime = { workspace = true } +simplex-core = { workspace = true } +simplex-sdk = { workspace = true } +simplex-config = { workspace = true } + +thiserror = { workspace = true } +simplicityhl = { workspace = true } +electrsd = { workspace = true } diff --git a/crates/test/src/common.rs b/crates/test/src/common.rs new file mode 100644 index 0000000..0cd6630 --- /dev/null +++ b/crates/test/src/common.rs @@ -0,0 +1,28 @@ +pub const DEFAULT_SAT_AMOUNT_FAUCET: u64 = 100000; + +pub trait ElementsdParams { + fn get_bin_args(&self) -> Vec; +} + +pub struct DefaultElementsdParams; + +impl ElementsdParams for DefaultElementsdParams { + fn get_bin_args(&self) -> Vec { + vec![ + "-fallbackfee=0.0001".to_string(), + "-dustrelayfee=0.00000001".to_string(), + "-acceptdiscountct=1".to_string(), + "-rest".to_string(), + "-evbparams=simplicity:-1:::".to_string(), // Enable Simplicity from block 0 + "-minrelaytxfee=0".to_string(), // test tx with no fees/asset fees + "-blockmintxfee=0".to_string(), // test tx with no fees/asset fees + "-chain=liquidregtest".to_string(), + "-txindex=1".to_string(), + "-validatepegin=0".to_string(), + "-initialfreecoins=2100000000000000".to_string(), + "-listen=1".to_string(), + "-txindex=1".to_string(), + // "-disablewallet=0".to_string(), + ] + } +} diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs new file mode 100644 index 0000000..57b0cda --- /dev/null +++ b/crates/test/src/error.rs @@ -0,0 +1,13 @@ +use simplex_runtime::ExplorerError; + +#[derive(thiserror::Error, Debug)] +pub enum TestError { + #[error("Explorer error occurred: {0}")] + Explorer(#[from] ExplorerError), + + #[error("Unhealthy rpc connection, error: {0}")] + UnhealthyRpc(ExplorerError), + + #[error("Node failed to start, error: {0}")] + NodeFailedToStart(String), +} diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs new file mode 100644 index 0000000..2547057 --- /dev/null +++ b/crates/test/src/lib.rs @@ -0,0 +1,131 @@ +mod common; +mod error; +mod testing; + +pub use common::*; +pub use error::*; + +use bitcoind::bitcoincore_rpc::{Auth, Client}; +use bitcoind::{BitcoinD, Conf}; +use electrsd::bitcoind; +use simplex_config::Config; +use simplex_core::SimplicityNetwork; +use simplex_runtime::elements_rpc::ElementsRpcClient; +use simplicityhl::elements::secp256k1_zkp::PublicKey; +use simplicityhl::elements::{Address, AssetId}; +use std::path::{Path, PathBuf}; + +#[derive(Hash, Clone, Debug, Eq, PartialEq)] +pub struct User { + pubkey: PublicKey, +} + +pub enum TestProvider { + ConfiguredNode { node: BitcoinD, network: SimplicityNetwork }, + CustomRpc(ElementsRpcClient), +} + +pub enum ConfigOption<'a> { + DefaultRegtest, + CustomConfRegtest { conf: Conf<'a> }, + CustomRpcUrlRegtest { url: String, auth: Auth }, +} + +impl TestProvider { + pub fn init(init_option: ConfigOption) -> Result { + let rpc = match init_option { + ConfigOption::DefaultRegtest => { + let node = Self::create_default_node(); + let network = SimplicityNetwork::default_regtest(); + Self::ConfiguredNode { node, network } + } + ConfigOption::CustomConfRegtest { conf } => { + let node = Self::create_node(conf, Self::get_bin_path())?; + let network = SimplicityNetwork::default_regtest(); + Self::ConfiguredNode { node, network } + } + ConfigOption::CustomRpcUrlRegtest { auth, url: rpc_url } => { + let network = SimplicityNetwork::default_regtest(); + Self::CustomRpc(ElementsRpcClient::new(network, &rpc_url, auth)?) + } + }; + + if let Err(e) = ElementsRpcClient::blockchain_info(rpc.as_ref()) { + return Err(TestError::UnhealthyRpc(e)); + } + Ok(rpc) + } + + // TODO: is it ok? + pub fn obtain_test_config() -> Config { + todo!() + } + + pub fn get_bin_path() -> PathBuf { + // TODO: change binary into installed one in $HOME dir + const ELEMENTSD_BIN_PATH: &str = "../../assets/elementsd"; + const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); + + Path::new(MANIFEST_DIR).join(ELEMENTSD_BIN_PATH) + } + + fn create_default_node() -> BitcoinD { + let mut conf = Conf::default(); + let bin_args = common::DefaultElementsdParams {}.get_bin_args(); + + conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.network = "liquidregtest"; + conf.p2p = bitcoind::P2P::Yes; + + BitcoinD::with_conf(Self::get_bin_path(), &conf).unwrap() + } + + pub fn create_default_node_with_stdin() -> BitcoinD { + let mut conf = Conf::default(); + let bin_args = common::DefaultElementsdParams {}.get_bin_args(); + + conf.args = bin_args.iter().map(|x| x.as_ref()).collect::>(); + conf.view_stdout = true; + conf.attempts = 2; + conf.network = "liquidregtest"; + conf.p2p = bitcoind::P2P::Yes; + + BitcoinD::with_conf(Self::get_bin_path(), &conf).unwrap() + } + + fn create_node(conf: Conf, bin_path: PathBuf) -> Result { + BitcoinD::with_conf(bin_path, &conf).map_err(|e| TestError::NodeFailedToStart(e.to_string())) + } + + pub fn client(&self) -> &Client { + match self { + TestProvider::ConfiguredNode { node, .. } => &node.client, + TestProvider::CustomRpc(x) => x.client(), + } + } + + pub fn network(&self) -> SimplicityNetwork { + match self { + TestProvider::ConfiguredNode { network, .. } => *network, + TestProvider::CustomRpc(x) => x.network(), + } + } +} + +impl TestProvider { + pub fn fund(satoshi: u64, address: Option
, asset: Option) { + todo!() + } + + pub fn get_height() {} + + pub fn get_blockchain_info() { + todo!() + } +} + +impl AsRef for TestProvider { + fn as_ref(&self) -> &Client { + self.client() + } +} diff --git a/crates/test/src/testing/config.rs b/crates/test/src/testing/config.rs new file mode 100644 index 0000000..40dd9cc --- /dev/null +++ b/crates/test/src/testing/config.rs @@ -0,0 +1 @@ +pub struct ConfigBuilder {} diff --git a/crates/test/src/testing/mod.rs b/crates/test/src/testing/mod.rs new file mode 100644 index 0000000..295f12b --- /dev/null +++ b/crates/test/src/testing/mod.rs @@ -0,0 +1,5 @@ +mod config; +mod rpc_provider; + +pub use config::*; +pub use rpc_provider::*; diff --git a/crates/test/src/testing/rpc_provider.rs b/crates/test/src/testing/rpc_provider.rs new file mode 100644 index 0000000..b589060 --- /dev/null +++ b/crates/test/src/testing/rpc_provider.rs @@ -0,0 +1,20 @@ +use simplex_sdk::error::SimplexError; +use simplex_sdk::provider::Provider; +use simplicityhl::elements::{Transaction, Txid}; +use std::collections::HashMap; + +pub struct TestRpcProvider {} + +impl Provider for TestRpcProvider { + fn broadcast_transaction(&self, tx: &Transaction) -> Result { + todo!() + } + + fn fetch_fee_estimates(&self) -> Result, SimplexError> { + todo!() + } + + fn fetch_transaction(&self, txid: Txid) -> Result { + todo!() + } +}