diff --git a/Cargo.lock b/Cargo.lock index b70ff78..fd550a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -165,6 +165,19 @@ dependencies = [ "virtue", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + [[package]] name = "bitcoin" version = "0.32.8" @@ -172,6 +185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", + "base64 0.21.7", "bech32", "bitcoin-internals", "bitcoin-io", @@ -294,9 +308,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -348,9 +362,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.59" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -358,9 +372,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.59" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -377,7 +391,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -458,9 +472,9 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.0", "block2", @@ -476,7 +490,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -495,6 +509,12 @@ dependencies = [ "simplicityhl-core", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -538,6 +558,20 @@ dependencies = [ "bech32", "bitcoin", "secp256k1-zkp", + "serde", + "serde_json", +] + +[[package]] +name = "elements-miniscript" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571fa105690f83c7833df2109eb2e14ca0e62d633d2624ffcb166ff18a3da870" +dependencies = [ + "bitcoin", + "elements", + "miniscript", + "serde", ] [[package]] @@ -810,7 +844,7 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "tokio", "tokio-rustls", @@ -1000,9 +1034,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" dependencies = [ "once_cell", "wasm-bindgen", @@ -1169,9 +1203,9 @@ checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -1203,28 +1237,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "options" -version = "0.1.0" -dependencies = [ - "simplex", - "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", -] - [[package]] name = "outref" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "p2pk" -version = "0.1.0" -dependencies = [ - "simplex", - "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", -] - [[package]] name = "pathdiff" version = "0.2.3" @@ -1267,7 +1285,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1282,9 +1300,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1323,7 +1341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1381,7 +1399,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", + "rustls 0.23.37", "socket2", "thiserror", "tokio", @@ -1401,7 +1419,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "slab", "thiserror", @@ -1507,7 +1525,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.14", - "regex-syntax 0.8.9", + "regex-syntax 0.8.10", ] [[package]] @@ -1529,7 +1547,7 @@ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.9", + "regex-syntax 0.8.10", ] [[package]] @@ -1540,9 +1558,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -1564,7 +1582,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "serde", "serde_json", @@ -1642,9 +1660,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -1747,6 +1765,7 @@ dependencies = [ "rand 0.8.5", "secp256k1", "secp256k1-zkp-sys", + "serde", ] [[package]] @@ -1792,7 +1811,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1902,7 +1921,7 @@ version = "0.1.0" dependencies = [ "serde", "simplex-macros-core", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1917,7 +1936,7 @@ dependencies = [ "serde", "simplex-test", "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", - "syn 2.0.116", + "syn 2.0.117", "thiserror", ] @@ -1942,7 +1961,12 @@ name = "simplex-sdk" version = "0.1.0" dependencies = [ "async-trait", + "bip39", + "dyn-clone", + "elements-miniscript", + "hex", "minreq", + "serde", "sha2", "simplex-provider", "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", @@ -1967,6 +1991,7 @@ name = "simplex-user" version = "0.1.0" dependencies = [ "simplex-sdk", + "simplicityhl 0.4.1 (git+https://github.com/ikripaka/SimplicityHL/?branch=feature%2Frich-params)", "thiserror", ] @@ -2110,9 +2135,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2136,7 +2161,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2184,7 +2209,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2275,7 +2300,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2284,7 +2309,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls 0.23.37", "tokio", ] @@ -2427,7 +2452,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2508,6 +2533,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -2615,9 +2649,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" dependencies = [ "cfg-if", "once_cell", @@ -2628,9 +2662,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" dependencies = [ "cfg-if", "futures-util", @@ -2642,9 +2676,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2652,22 +2686,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" dependencies = [ "unicode-ident", ] @@ -2708,9 +2742,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" dependencies = [ "js-sys", "wasm-bindgen", @@ -2969,7 +3003,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.116", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2985,7 +3019,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3052,28 +3086,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3093,7 +3127,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -3133,7 +3167,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 397eb18..35471c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,7 @@ resolver = "3" members = [ "crates/*", - "examples/options", - "examples/p2pk", - "examples/test_usage", + "example", ] [workspace.package] @@ -21,6 +19,7 @@ simplex-macros = { path = "./crates/macros" } simplex-test = { path = "./crates/test" } simplex-sdk = { path = "./crates/sdk" } simplex = { path = "./crates/simplex" } +simplex-user = { path = "./crates/user" } async-trait = { version = "0.1.89" } bincode = { version = "2.0.1", features = ["serde"] } diff --git a/Simplex.toml b/Simplex.toml deleted file mode 100644 index 562808f..0000000 --- a/Simplex.toml +++ /dev/null @@ -1,4 +0,0 @@ -network = "liquidtestnet" - -[test] -exe_path = "../assets/bin" \ No newline at end of file diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index b331c99..4e6624b 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -24,6 +24,7 @@ electrsd = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } toml = { workspace = true } + anyhow = "1" dotenvy = "0.15" clap = { version = "4", features = ["derive", "env"] } @@ -36,4 +37,4 @@ glob = { version = "0.3.3"} [dev-dependencies] tracing = { workspace = true } tracing-appender = { version = "0.2.3" } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } \ No newline at end of file +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/crates/cli/README.md b/crates/cli/README.md deleted file mode 100644 index d2dff83..0000000 --- a/crates/cli/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Simplex-CLI - -CLI instrument to support easy building and creation of contracts on SimplicityHL. - -## License - -Dual-licensed under either of: -- Apache License, Version 2.0 (Apache-2.0) -- MIT license (MIT) - -at your option. \ No newline at end of file diff --git a/Simplex.example.toml b/crates/cli/Simplex.example.toml similarity index 100% rename from Simplex.example.toml rename to crates/cli/Simplex.example.toml diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 41a31f1..a03a5e8 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -5,7 +5,7 @@ use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; -pub const DEFAULT_CONFIG: &str = include_str!("../../../Simplex.example.toml"); +pub const DEFAULT_CONFIG: &str = include_str!("../Simplex.example.toml"); const CONFIG_FILENAME: &str = "Simplex.toml"; #[derive(thiserror::Error, Debug)] diff --git a/crates/macros-core/Cargo.toml b/crates/macros-core/Cargo.toml index 5c70ea1..d50228a 100644 --- a/crates/macros-core/Cargo.toml +++ b/crates/macros-core/Cargo.toml @@ -14,14 +14,14 @@ serde = ["bincode", "dep:serde"] default = ["bincode", "serde"] [dependencies] +simplicityhl = { workspace = true } +simplex-test = { workspace = true } +thiserror = { workspace = true } + 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 = { workspace = true } quote = { version = "1.0.44" } -simplicityhl = { workspace = true } -simplex-test = { workspace = true } serde = { version = "1.0.228", optional = true } pathdiff = { version = "0.2.3" } prettyplease = { version = "0.2.37" } - diff --git a/crates/macros-core/src/attr/codegen.rs b/crates/macros-core/src/attr/codegen.rs index a944b50..c089898 100644 --- a/crates/macros-core/src/attr/codegen.rs +++ b/crates/macros-core/src/attr/codegen.rs @@ -113,7 +113,7 @@ impl WitnessStruct { use ::simplicityhl::str::WitnessName; use ::simplicityhl::types::TypeConstructible; use ::simplicityhl::value::ValueConstructible; - use ::simplex::simplex_sdk::arguments::ArgumentsTrait; + use ::simplex::simplex_sdk::program::ArgumentsTrait; use ::simplex::bincode::*; }, struct_token_stream: quote! { @@ -134,7 +134,7 @@ impl WitnessStruct { } - impl ::simplex::simplex_sdk::arguments::ArgumentsTrait for #struct_name { + impl ::simplex::simplex_sdk::program::ArgumentsTrait for #struct_name { /// Build Simplicity arguments for contract instantiation. #[must_use] fn build_arguments(&self) -> ::simplicityhl::Arguments { @@ -190,7 +190,7 @@ impl WitnessStruct { use simplicityhl::str::WitnessName; use simplicityhl::types::TypeConstructible; use simplicityhl::value::ValueConstructible; - use ::simplex::simplex_sdk::witness::WitnessTrait; + use ::simplex::simplex_sdk::program::WitnessTrait; }, struct_token_stream: quote! { #generated_struct @@ -209,7 +209,7 @@ impl WitnessStruct { } } - impl ::simplex::simplex_sdk::witness::WitnessTrait for #struct_name { + impl ::simplex::simplex_sdk::program::WitnessTrait for #struct_name { /// Build Simplicity witness values for contract execution. #[must_use] fn build_witness(&self) -> ::simplicityhl::WitnessValues { diff --git a/crates/macros-core/src/env/mod.rs b/crates/macros-core/src/env/mod.rs index bccccaa..8adef87 100644 --- a/crates/macros-core/src/env/mod.rs +++ b/crates/macros-core/src/env/mod.rs @@ -102,28 +102,27 @@ impl<'b> CodeGenerator { let code = quote! { use simplex::simplex_macros::include_simf; - use simplex::simplex_sdk::arguments::ArgumentsTrait; - use simplex::simplex_sdk::program::Program; + use simplex::simplex_sdk::program::{ArgumentsTrait, Program}; use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; - pub struct #program_name<'a> { - program: Program<'a>, + pub struct #program_name { + program: Program, } - impl<'a> #program_name<'a> { + impl #program_name { pub const SOURCE: &'static str = #include_simf_module::#include_simf_source_const; - pub fn new(public_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { + pub fn new(public_key: XOnlyPublicKey, arguments: impl ArgumentsTrait + 'static) -> Self { Self { - program: Program::new(Self::SOURCE, public_key, arguments), + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), } } - pub fn get_program(&self) -> &Program<'a> { + pub fn get_program(&self) -> &Program { &self.program } - pub fn get_program_mut(&mut self) -> &mut Program<'a> { + pub fn get_program_mut(&mut self) -> &mut Program { &mut self.program } } diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index de24136..97d32f5 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -26,5 +26,6 @@ workspace = true [dependencies] simplex-macros-core = { workspace = true } + serde = { version = "1.0.228", optional = true } syn = { version = "2.0.114", default-features = false, features = ["parsing", "proc-macro"] } diff --git a/crates/macros/README.md b/crates/macros/README.md deleted file mode 100644 index c52f23b..0000000 --- a/crates/macros/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Simplex SDK - - - -## License - -Dual-licensed under either of: -- Apache License, Version 2.0 (Apache-2.0) -- MIT license (MIT) - -at your option. \ No newline at end of file diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index 175f2d0..20fa314 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -17,6 +17,7 @@ workspace = true simplicityhl = { workspace = true } thiserror = { workspace = true } electrsd = { workspace = true } + async-trait = { version = "0.1.89" } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } minreq = { version = "2.14", features = ["https", "json-using-serde"] } diff --git a/crates/provider/README.md b/crates/provider/README.md deleted file mode 100644 index 40ed1c5..0000000 --- a/crates/provider/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Simplex-explorer - -Waterfall API doc - [link][1]. -Esplora API doc - [link][2]. - -[1]: https://github.com/RCasatta/waterfalls/blob/master/API.md -[1]: https://github.com/Blockstream/esplora/blob/master/API.md \ No newline at end of file diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 5e16a89..9880bed 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -17,3 +17,9 @@ thiserror = { workspace = true } sha2 = { workspace = true } minreq = { workspace = true } simplicityhl = { workspace = true } + +hex = { version = "0.4" } +serde = { version = "1.0.228", features = ["derive"]} +elements-miniscript = { version = "0.4", features = ["base64", "serde"] } +bip39 = { version = "2.0.0", features = ["rand"] } +dyn-clone = { version = "1.0.20" } diff --git a/crates/sdk/src/arguments.rs b/crates/sdk/src/arguments.rs deleted file mode 100644 index c2ca884..0000000 --- a/crates/sdk/src/arguments.rs +++ /dev/null @@ -1,5 +0,0 @@ -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 index 87339cf..01aab2d 100644 --- a/crates/sdk/src/constants.rs +++ b/crates/sdk/src/constants.rs @@ -82,6 +82,10 @@ impl SimplicityNetwork { } } + pub fn is_mainnet(&self) -> bool { + self == &Self::Liquid + } + pub const fn address_params(&self) -> &'static elements::AddressParams { match self { Self::Liquid => &elements::AddressParams::LIQUID, diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs deleted file mode 100644 index 64f606b..0000000 --- a/crates/sdk/src/error.rs +++ /dev/null @@ -1,66 +0,0 @@ -use simplex_provider::ExplorerError; -use simplicityhl::elements::secp256k1_zkp; -use simplicityhl::simplicity::hex::HexToArrayError; - -#[derive(Debug, thiserror::Error)] -pub enum SimplexError { - #[error("Fee amount is too low: {0}")] - PstFailure(#[from] simplicityhl::elements::pset::Error), - - #[error("Fee amount is too low: {0}")] - DustAmount(u64), - - #[error("Not enough fee amount {0} to cover transaction costs: {1}")] - NotEnoughFeeAmount(u64, u64), - - #[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), - - #[error("Failed to execute rpc query, err: '{0}'")] - RpcExecution(String), - - #[error("Hex to array error: '{0}'")] - HexToArray(#[from] HexToArrayError), - - #[error("Failed to execute provider method: '{method}', err: '{err}'")] - ProviderError { method: String, err: Box }, -} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 7b167c9..96ab949 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,12 +1,7 @@ -#![warn(clippy::all, clippy::pedantic)] - -pub mod arguments; pub mod constants; -pub mod error; pub mod presets; pub mod program; +pub mod transaction; pub mod provider; pub mod signer; pub mod utils; -pub mod witness; -pub mod witness_transaction; diff --git a/crates/sdk/src/presets/mod.rs b/crates/sdk/src/presets/mod.rs index 5083e09..90c5352 100644 --- a/crates/sdk/src/presets/mod.rs +++ b/crates/sdk/src/presets/mod.rs @@ -1 +1,4 @@ pub mod p2pk; + +pub use p2pk::P2PK; +pub use p2pk::p2pk_build::{P2PKArguments, P2PKWitness}; diff --git a/crates/sdk/src/presets/p2pk.rs b/crates/sdk/src/presets/p2pk.rs index 411f0be..7f70a04 100644 --- a/crates/sdk/src/presets/p2pk.rs +++ b/crates/sdk/src/presets/p2pk.rs @@ -1,121 +1,57 @@ -use crate::arguments::ArgumentsTrait; +use crate::program::ArgumentsTrait; use crate::program::Program; use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; // TODO macro -pub struct P2PK<'a> { - program: Program<'a>, +pub struct P2PK { + program: Program, } -impl<'a> P2PK<'a> { +impl P2PK { pub const SOURCE: &'static str = include_str!("./simf/p2pk.simf"); - pub fn new(public_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { + pub fn new(public_key: XOnlyPublicKey, arguments: impl ArgumentsTrait + 'static) -> Self { Self { - program: Program::new(Self::SOURCE, public_key, arguments), + program: Program::new(Self::SOURCE, public_key, Box::new(arguments)), } } - pub fn get_program(&self) -> &Program<'a> { + pub fn get_program(&self) -> &Program { &self.program } - pub fn get_program_mut(&mut self) -> &mut Program<'a> { + pub fn get_program_mut(&mut self) -> &mut Program { &mut self.program } } pub mod p2pk_build { - use crate::arguments::ArgumentsTrait; - use crate::witness::WitnessTrait; + use crate::program::ArgumentsTrait; + use crate::program::WitnessTrait; use simplicityhl::num::U256; use simplicityhl::str::WitnessName; - use simplicityhl::types::TypeConstructible; use simplicityhl::value::UIntValue; use simplicityhl::value::ValueConstructible; - use simplicityhl::{Arguments, ResolvedType, Value, WitnessValues}; + use simplicityhl::{Arguments, Value, WitnessValues}; use std::collections::HashMap; + #[derive(Clone)] pub struct P2PKWitness { pub signature: [u8; 64usize], } + #[derive(Clone)] pub struct P2PKArguments { pub public_key: [u8; 32], } impl WitnessTrait for P2PKWitness { fn build_witness(&self) -> WitnessValues { - WitnessValues::from(HashMap::from([(WitnessName::from_str_unchecked("SIGNATURE"), { - let elements = [ - Value::from(UIntValue::U8(self.signature[0])), - Value::from(UIntValue::U8(self.signature[1])), - Value::from(UIntValue::U8(self.signature[2])), - Value::from(UIntValue::U8(self.signature[3])), - Value::from(UIntValue::U8(self.signature[4])), - Value::from(UIntValue::U8(self.signature[5])), - Value::from(UIntValue::U8(self.signature[6])), - Value::from(UIntValue::U8(self.signature[7])), - Value::from(UIntValue::U8(self.signature[8])), - Value::from(UIntValue::U8(self.signature[9])), - Value::from(UIntValue::U8(self.signature[10])), - Value::from(UIntValue::U8(self.signature[11])), - Value::from(UIntValue::U8(self.signature[12])), - Value::from(UIntValue::U8(self.signature[13])), - Value::from(UIntValue::U8(self.signature[14])), - Value::from(UIntValue::U8(self.signature[15])), - Value::from(UIntValue::U8(self.signature[16])), - Value::from(UIntValue::U8(self.signature[17])), - Value::from(UIntValue::U8(self.signature[18])), - Value::from(UIntValue::U8(self.signature[19])), - Value::from(UIntValue::U8(self.signature[20])), - Value::from(UIntValue::U8(self.signature[21])), - Value::from(UIntValue::U8(self.signature[22])), - Value::from(UIntValue::U8(self.signature[23])), - Value::from(UIntValue::U8(self.signature[24])), - Value::from(UIntValue::U8(self.signature[25])), - Value::from(UIntValue::U8(self.signature[26])), - Value::from(UIntValue::U8(self.signature[27])), - Value::from(UIntValue::U8(self.signature[28])), - Value::from(UIntValue::U8(self.signature[29])), - Value::from(UIntValue::U8(self.signature[30])), - Value::from(UIntValue::U8(self.signature[31])), - Value::from(UIntValue::U8(self.signature[32])), - Value::from(UIntValue::U8(self.signature[33])), - Value::from(UIntValue::U8(self.signature[34])), - Value::from(UIntValue::U8(self.signature[35])), - Value::from(UIntValue::U8(self.signature[36])), - Value::from(UIntValue::U8(self.signature[37])), - Value::from(UIntValue::U8(self.signature[38])), - Value::from(UIntValue::U8(self.signature[39])), - Value::from(UIntValue::U8(self.signature[40])), - Value::from(UIntValue::U8(self.signature[41])), - Value::from(UIntValue::U8(self.signature[42])), - Value::from(UIntValue::U8(self.signature[43])), - Value::from(UIntValue::U8(self.signature[44])), - Value::from(UIntValue::U8(self.signature[45])), - Value::from(UIntValue::U8(self.signature[46])), - Value::from(UIntValue::U8(self.signature[47])), - Value::from(UIntValue::U8(self.signature[48])), - Value::from(UIntValue::U8(self.signature[49])), - Value::from(UIntValue::U8(self.signature[50])), - Value::from(UIntValue::U8(self.signature[51])), - Value::from(UIntValue::U8(self.signature[52])), - Value::from(UIntValue::U8(self.signature[53])), - Value::from(UIntValue::U8(self.signature[54])), - Value::from(UIntValue::U8(self.signature[55])), - Value::from(UIntValue::U8(self.signature[56])), - Value::from(UIntValue::U8(self.signature[57])), - Value::from(UIntValue::U8(self.signature[58])), - Value::from(UIntValue::U8(self.signature[59])), - Value::from(UIntValue::U8(self.signature[60])), - Value::from(UIntValue::U8(self.signature[61])), - Value::from(UIntValue::U8(self.signature[62])), - Value::from(UIntValue::U8(self.signature[63])), - ]; - Value::array(elements, ResolvedType::u8()) - })])) + WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("SIGNATURE"), + Value::byte_array(self.signature), + )])) } } diff --git a/crates/sdk/src/program/arguments.rs b/crates/sdk/src/program/arguments.rs new file mode 100644 index 0000000..a855e46 --- /dev/null +++ b/crates/sdk/src/program/arguments.rs @@ -0,0 +1,8 @@ +use simplicityhl::Arguments; +use dyn_clone::DynClone; + +pub trait ArgumentsTrait: DynClone { + fn build_arguments(&self) -> Arguments; +} + +dyn_clone::clone_trait_object!(ArgumentsTrait); diff --git a/crates/sdk/src/program/error.rs b/crates/sdk/src/program/error.rs new file mode 100644 index 0000000..b80e8e1 --- /dev/null +++ b/crates/sdk/src/program/error.rs @@ -0,0 +1,29 @@ +#[derive(Debug, thiserror::Error)] +pub enum ProgramError { + #[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("Failed to extract tx from pst: {0}")] + TxExtraction(#[from] simplicityhl::elements::pset::Error), + + #[error("Input index exceeds u32 maximum: {0}")] + InputIndexOverflow(#[from] std::num::TryFromIntError), +} diff --git a/crates/sdk/src/program/mod.rs b/crates/sdk/src/program/mod.rs new file mode 100644 index 0000000..7d924e5 --- /dev/null +++ b/crates/sdk/src/program/mod.rs @@ -0,0 +1,9 @@ +pub mod arguments; +pub mod error; +pub mod program; +pub mod witness; + +pub use arguments::ArgumentsTrait; +pub use error::ProgramError; +pub use program::{Program, ProgramTrait}; +pub use witness::WitnessTrait; diff --git a/crates/sdk/src/program.rs b/crates/sdk/src/program/program.rs similarity index 59% rename from crates/sdk/src/program.rs rename to crates/sdk/src/program/program.rs index 14e7cc9..6ccc521 100644 --- a/crates/sdk/src/program.rs +++ b/crates/sdk/src/program/program.rs @@ -2,83 +2,86 @@ use std::sync::Arc; use sha2::{Digest, Sha256}; +use dyn_clone::DynClone; + use simplicityhl::CompiledProgram; use simplicityhl::WitnessValues; -use simplicityhl::elements::{Address, Script, Transaction, TxInWitness, TxOut, script, taproot}; +use simplicityhl::elements::pset::PartiallySignedTransaction; +use simplicityhl::elements::{Address, Script, Transaction, 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 super::arguments::ArgumentsTrait; +use super::error::ProgramError; use crate::constants::SimplicityNetwork; -use crate::error::SimplexError; -pub trait ProgramTrait { +pub trait ProgramTrait: DynClone { fn get_env( &self, - tx: &Transaction, - utxos: &[TxOut], + pst: &PartiallySignedTransaction, input_index: usize, - network: SimplicityNetwork, - ) -> Result>, SimplexError>; + network: &SimplicityNetwork, + ) -> Result>, ProgramError>; fn execute( &self, - witness: WitnessValues, - tx: &Transaction, - utxos: &[TxOut], + pst: &PartiallySignedTransaction, + witness: &WitnessValues, input_index: usize, - network: SimplicityNetwork, - ) -> Result<(Arc>, Value), SimplexError>; + network: &SimplicityNetwork, + ) -> Result<(Arc>, Value), ProgramError>; fn finalize( &self, - witness: WitnessValues, - tx: Transaction, - utxos: &[TxOut], + pst: &PartiallySignedTransaction, + witness: &WitnessValues, input_index: usize, - network: SimplicityNetwork, - ) -> Result; + network: &SimplicityNetwork, + ) -> Result>, ProgramError>; } -pub struct Program<'a> { +#[derive(Clone)] +pub struct Program { source: &'static str, - pub_key: &'a XOnlyPublicKey, - arguments: &'a dyn ArgumentsTrait, + pub_key: XOnlyPublicKey, + arguments: Box, } -impl<'a> ProgramTrait for Program<'a> { +dyn_clone::clone_trait_object!(ProgramTrait); + +impl ProgramTrait for Program { fn get_env( &self, - tx: &Transaction, - utxos: &[TxOut], + pst: &PartiallySignedTransaction, input_index: usize, - network: SimplicityNetwork, - ) -> Result>, SimplexError> { + network: &SimplicityNetwork, + ) -> Result>, ProgramError> { let genesis_hash = network.genesis_block_hash(); let cmr = self.load()?.commit().cmr(); + let utxos: Vec = pst.inputs().iter().filter_map(|x| x.witness_utxo.clone()).collect(); if utxos.len() <= input_index { - return Err(SimplexError::UtxoIndexOutOfBounds { + return Err(ProgramError::UtxoIndexOutOfBounds { input_index, utxo_count: utxos.len(), }); } let target_utxo = &utxos[input_index]; - let script_pubkey = self.get_tr_address(network)?.script_pubkey(); + let script_pubkey = self.get_tr_address(network.clone())?.script_pubkey(); if target_utxo.script_pubkey != script_pubkey { - return Err(SimplexError::ScriptPubkeyMismatch { + return Err(ProgramError::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()), + Arc::new(pst.extract_tx()?), utxos .iter() .map(|utxo| ElementsUtxo { @@ -97,60 +100,51 @@ impl<'a> ProgramTrait for Program<'a> { fn execute( &self, - witness: WitnessValues, - tx: &Transaction, - utxos: &[TxOut], + pst: &PartiallySignedTransaction, + witness: &WitnessValues, input_index: usize, - network: SimplicityNetwork, - ) -> Result<(Arc>, Value), SimplexError> { + network: &SimplicityNetwork, + ) -> Result<(Arc>, Value), ProgramError> { let satisfied = self .load()? - .satisfy(witness) - .map_err(SimplexError::WitnessSatisfaction)?; + .satisfy(witness.clone()) + .map_err(ProgramError::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 env = self.get_env(pst, 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)?; + let result = mac.exec(&pruned, &env)?; Ok((pruned, result)) } fn finalize( &self, - witness: WitnessValues, - mut tx: Transaction, - utxos: &[TxOut], + pst: &PartiallySignedTransaction, + witness: &WitnessValues, input_index: usize, - network: SimplicityNetwork, - ) -> Result { - let pruned = self.execute(witness, &tx, utxos, input_index, network)?.0; + network: &SimplicityNetwork, + ) -> Result>, ProgramError> { + let pruned = self.execute(&pst, witness, 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) + Ok(vec![ + simplicity_witness_bytes, + simplicity_program_bytes, + cmr.as_ref().to_vec(), + self.control_block()?.serialize(), + ]) } } -impl<'a> Program<'a> { - pub fn new(source: &'static str, pub_key: &'a XOnlyPublicKey, arguments: &'a impl ArgumentsTrait) -> Self { +impl Program { + pub fn new(source: &'static str, pub_key: XOnlyPublicKey, arguments: Box) -> Self { Self { source: source, pub_key: pub_key, @@ -158,7 +152,7 @@ impl<'a> Program<'a> { } } - pub fn get_tr_address(&self, network: SimplicityNetwork) -> Result { + pub fn get_tr_address(&self, network: SimplicityNetwork) -> Result { let spend_info = self.taproot_spending_info()?; Ok(Address::p2tr( @@ -170,11 +164,11 @@ impl<'a> Program<'a> { )) } - pub fn get_script_pubkey(&self, network: SimplicityNetwork) -> Result { + 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> { + pub fn get_script_hash(&self, network: SimplicityNetwork) -> Result<[u8; 32], ProgramError> { let script = self.get_script_pubkey(network)?; let mut hasher = Sha256::new(); @@ -182,20 +176,20 @@ impl<'a> Program<'a> { Ok(hasher.finalize().into()) } - fn load(&self) -> Result { + fn load(&self) -> Result { let compiled = CompiledProgram::new(self.source, self.arguments.build_arguments(), true) - .map_err(SimplexError::Compilation)?; + .map_err(ProgramError::Compilation)?; Ok(compiled) } - fn script_version(&self) -> Result<(Script, taproot::LeafVersion), SimplexError> { + fn script_version(&self) -> Result<(Script, taproot::LeafVersion), ProgramError> { 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 { + fn taproot_spending_info(&self) -> Result { let builder = taproot::TaprootBuilder::new(); let (script, version) = self.script_version()?; @@ -204,11 +198,11 @@ impl<'a> Program<'a> { .expect("tap tree should be valid"); Ok(builder - .finalize(secp256k1::SECP256K1, *self.pub_key) + .finalize(secp256k1::SECP256K1, self.pub_key) .expect("tap tree should be valid")) } - fn control_block(&self) -> Result { + fn control_block(&self) -> Result { let info = self.taproot_spending_info()?; let script_ver = self.script_version()?; diff --git a/crates/sdk/src/program/witness.rs b/crates/sdk/src/program/witness.rs new file mode 100644 index 0000000..b6d0735 --- /dev/null +++ b/crates/sdk/src/program/witness.rs @@ -0,0 +1,8 @@ +use simplicityhl::WitnessValues; +use dyn_clone::DynClone; + +pub trait WitnessTrait: DynClone { + fn build_witness(&self) -> WitnessValues; +} + +dyn_clone::clone_trait_object!(WitnessTrait); diff --git a/crates/sdk/src/provider/error.rs b/crates/sdk/src/provider/error.rs new file mode 100644 index 0000000..81c9011 --- /dev/null +++ b/crates/sdk/src/provider/error.rs @@ -0,0 +1,17 @@ +#[derive(Debug, thiserror::Error)] +pub enum ProviderError { + #[error("HTTP request failed: {0}")] + Request(String), + + #[error("Couldn't wait for the transaction to be confirmed")] + Confirmation(), + + #[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/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs index 388c180..e6f2b1e 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -1,56 +1,230 @@ -use crate::error::SimplexError; -use crate::provider::{ProviderAsync, ProviderSync}; -pub use simplex_provider::esplora::*; -use simplicityhl::elements::hex::ToHex; -use simplicityhl::elements::{Transaction, Txid}; use std::collections::HashMap; +use std::collections::HashSet; +use std::str::FromStr; +use std::thread::sleep; +use std::time::Duration; + +use simplicityhl::elements::hashes::{Hash, sha256}; + +use simplicityhl::elements::encode; +use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; + +use serde::Deserialize; + +pub use simplex_provider::esplora::*; + +use super::error::ProviderError; +use super::provider::ProviderTrait; + +#[derive(Clone)] +pub struct EsploraProvider { + esplora_url: String, +} + +#[derive(Deserialize)] +#[allow(dead_code)] +struct TxStatus { + confirmed: bool, + #[serde(default)] + block_height: Option, +} + +#[derive(Clone, Deserialize)] +#[allow(dead_code)] +struct UtxoStatus { + pub confirmed: bool, + #[serde(default)] + pub block_height: Option, + #[serde(default)] + pub block_hash: Option, + #[serde(default)] + pub block_time: Option, +} + +#[derive(Clone, Deserialize)] +#[allow(dead_code)] +struct EsploraUtxo { + pub txid: String, + pub vout: u32, + #[serde(default)] + pub value: Option, + #[serde(default)] + pub valuecommitment: Option, + #[serde(default)] + pub asset: Option, + #[serde(default)] + pub assetcommitment: Option, + pub status: UtxoStatus, +} -impl ProviderSync for EsploraClientSync { - fn broadcast_transaction(&self, tx: &Transaction) -> Result { - self.broadcast_tx(tx).map_err(|e| SimplexError::ProviderError { - method: "broadcast_tx".to_string(), - err: Box::new(e), - }) +impl EsploraProvider { + pub fn new(url: String) -> Self { + Self { esplora_url: url } } - fn fetch_fee_estimates(&self) -> Result, SimplexError> { - self.get_fee_estimates().map_err(|e| SimplexError::ProviderError { - method: "get_fee_estimates".to_string(), - err: Box::new(e), - }) + fn esplora_utxo_to_outpoint(&self, utxo: &EsploraUtxo) -> Result { + let txid = Txid::from_str(&utxo.txid).map_err(|e| ProviderError::InvalidTxid(e.to_string()))?; + + Ok(OutPoint::new(txid, utxo.vout)) } - fn fetch_transaction(&self, txid: &Txid) -> Result { - self.get_tx_elements(&txid.to_hex()) - .map_err(|e| SimplexError::ProviderError { - method: "get_tx_elements".to_string(), - err: Box::new(e), + fn populate_txouts_from_outpoints( + &self, + outpoints: &Vec, + ) -> Result, ProviderError> { + let set: HashSet<_> = outpoints.into_iter().collect(); + let mut map = HashMap::new(); + + // filter unique transactions + for point in set { + let tx = self.fetch_transaction(&point.txid)?; + map.insert(point.txid, tx); + } + + // populate TxOuts + Ok(outpoints + .iter() + .map(|point| { + ( + *point, + map.get(&point.txid).unwrap().output[point.vout as usize].clone(), + ) }) + .collect()) } } -#[async_trait::async_trait] -impl ProviderAsync for EsploraClientAsync { - async fn broadcast_transaction(&self, tx: &Transaction) -> Result { - self.broadcast_tx(tx).await.map_err(|e| SimplexError::ProviderError { - method: "broadcast_tx".to_string(), - err: Box::new(e), - }) +impl ProviderTrait 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| ProviderError::Request(e.to_string()))?; + + let status = response.status_code; + let body = response.as_str().unwrap_or("").trim().to_owned(); + + if !(200..300).contains(&status) { + return Err(ProviderError::BroadcastRejected { + status: status as u16, + url: format!("{}/tx", self.esplora_url), + message: body, + }); + } + + Ok(Txid::from_str(&body).map_err(|e| ProviderError::InvalidTxid(e.to_string()))?) + } + + fn wait(&self, txid: &Txid) -> Result<(), ProviderError> { + let status_url = format!("{}/tx/{}/status", self.esplora_url, txid); + + for _ in 1..10 { + let response = minreq::get(&status_url) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + sleep(Duration::from_secs(5)); + continue; + } + + let status: TxStatus = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; + + if status.confirmed { + return Ok(()); + } + + sleep(Duration::from_secs(10)); + } + + Err(ProviderError::Confirmation()) } - async fn fetch_fee_estimates(&self) -> Result, SimplexError> { - self.get_fee_estimates().await.map_err(|e| SimplexError::ProviderError { - method: "get_fee_estimates".to_string(), - err: Box::new(e), - }) + fn fetch_transaction(&self, txid: &Txid) -> Result { + let url = format!("{}/tx/{}/raw", self.esplora_url, txid); + let response = minreq::get(&url) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(ProviderError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let bytes = response.as_bytes(); + let tx: Transaction = encode::deserialize(bytes).map_err(|e| ProviderError::Deserialize(e.to_string()))?; + + Ok(tx) } - async fn fetch_transaction(&self, txid: &Txid) -> Result { - self.get_tx_elements(&txid.to_hex()) - .await - .map_err(|e| SimplexError::ProviderError { - method: "get_tx_elements".to_string(), - err: Box::new(e), - }) + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { + let url = format!("{}/address/{}/utxo", self.esplora_url, address); + let response = minreq::get(&url) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(ProviderError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let utxos: Vec = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; + let outpoints = utxos + .iter() + .map(|utxo| Ok(self.esplora_utxo_to_outpoint(&utxo)?)) + .collect::, ProviderError>>()?; + + Ok(self.populate_txouts_from_outpoints(&outpoints)?) + } + + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { + let hash = sha256::Hash::hash(script.as_bytes()); + let hash_bytes = hash.to_byte_array(); + let scripthash = hex::encode(hash_bytes); + + let url = format!("{}/scripthash/{}/utxo", self.esplora_url, scripthash); + let response = minreq::get(&url) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(ProviderError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let utxos: Vec = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; + let outpoints = utxos + .iter() + .map(|utxo| Ok(self.esplora_utxo_to_outpoint(&utxo)?)) + .collect::, ProviderError>>()?; + + Ok(self.populate_txouts_from_outpoints(&outpoints)?) + } + + fn fetch_fee_estimates(&self) -> Result, ProviderError> { + let url = format!("{}/fee-estimates", self.esplora_url); + let response = minreq::get(&url) + .send() + .map_err(|e| ProviderError::Request(e.to_string()))?; + + if response.status_code != 200 { + return Err(ProviderError::Request(format!( + "HTTP {}: {}", + response.status_code, response.reason_phrase + ))); + } + + let estimates: HashMap = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; + + Ok(estimates) } } diff --git a/crates/sdk/src/provider/mod.rs b/crates/sdk/src/provider/mod.rs index 5556328..f929382 100644 --- a/crates/sdk/src/provider/mod.rs +++ b/crates/sdk/src/provider/mod.rs @@ -1,173 +1,7 @@ -mod esplora; +pub mod error; +pub mod esplora; +pub mod provider; -use simplicityhl::elements::encode; -use simplicityhl::elements::hex::ToHex; -use simplicityhl::elements::{Transaction, Txid}; -use std::collections::HashMap; -use std::str::FromStr; - -use crate::constants::DEFAULT_FEE_RATE; -use crate::error::SimplexError; - -pub use simplex_provider::esplora::*; - -pub trait ProviderSync { - 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())) - } -} - -#[async_trait::async_trait] -pub trait ProviderAsync { - async fn broadcast_transaction(&self, tx: &Transaction) -> Result; - - async fn fetch_fee_estimates(&self) -> Result, SimplexError>; - - async fn fetch_transaction(&self, txid: &Txid) -> Result; - - async fn get_fee_rate(&self, target_blocks: u32) -> Result { - if target_blocks == 0 { - return Ok(DEFAULT_FEE_RATE); - } - - let estimates = self.fetch_fee_estimates().await?; - - 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 ProviderSync 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(Txid::from_str(&body)?) - } - - fn fetch_fee_estimates(&self) -> Result, SimplexError> { - let url = format!("{}/fee-estimates", self.esplora_url.clone()); - 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 = format!("{}/tx/{}/raw", self.esplora_url.clone(), txid.to_hex().as_str()); - 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) - } -} +pub use error::ProviderError; +pub use esplora::EsploraProvider; +pub use provider::ProviderTrait; diff --git a/crates/sdk/src/provider/provider.rs b/crates/sdk/src/provider/provider.rs new file mode 100644 index 0000000..ca0a92c --- /dev/null +++ b/crates/sdk/src/provider/provider.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; + +use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut, Txid}; + +use super::error::ProviderError; +use crate::constants::DEFAULT_FEE_RATE; + +pub trait ProviderTrait { + fn broadcast_transaction(&self, tx: &Transaction) -> Result; + + fn wait(&self, txid: &Txid) -> Result<(), ProviderError>; + + fn fetch_transaction(&self, txid: &Txid) -> Result; + + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError>; + + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError>; + + fn fetch_fee_estimates(&self) -> Result, ProviderError>; + + fn get_fee_rate(&self, target_blocks: u32) -> Result { + 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); + } + } + + Ok(DEFAULT_FEE_RATE) + } +} diff --git a/crates/sdk/src/signer.rs b/crates/sdk/src/signer.rs deleted file mode 100644 index d607309..0000000 --- a/crates/sdk/src/signer.rs +++ /dev/null @@ -1,67 +0,0 @@ -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 - } - - // TODO EIP-191 or EIP-712 messages - 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/signer/error.rs b/crates/sdk/src/signer/error.rs new file mode 100644 index 0000000..6f91f07 --- /dev/null +++ b/crates/sdk/src/signer/error.rs @@ -0,0 +1,51 @@ +use crate::program::ProgramError; +use crate::provider::ProviderError; +use crate::transaction::TransactionError; + +#[derive(Debug, thiserror::Error)] +pub enum SignerError { + #[error(transparent)] + Program(#[from] ProgramError), + + #[error(transparent)] + Provider(#[from] ProviderError), + + #[error(transparent)] + Transaction(#[from] TransactionError), + + #[error("Failed to parse a mnemonic: {0}")] + Mnemonic(String), + + #[error("Failed to extract tx from pst: {0}")] + TxExtraction(#[from] simplicityhl::elements::pset::Error), + + #[error("Failed to construct a message for the input spending: {0}")] + SighashConstruction(#[from] elements_miniscript::psbt::SighashError), + + #[error("Fee amount is too low: {0}")] + DustAmount(i64), + + #[error("Not enough fee amount {0} to cover transaction costs: {1}")] + NotEnoughFeeAmount(i64, u64), + + #[error("Not enough funds on account to cover transaction costs: {0}")] + NotEnoughFunds(u64), + + #[error("Invalid secret key")] + InvalidSecretKey(#[from] simplicityhl::elements::secp256k1_zkp::UpstreamError), + + #[error("Failed to derive a private key: {0}")] + PrivateKeyDerivation(#[from] elements_miniscript::bitcoin::bip32::Error), + + #[error("Failed to construct a derivation path: {0}")] + DerivationPath(String), + + #[error("Failed to construct a wpkh descriptor: {0}")] + WpkhDescriptor(String), + + #[error("Failed to convert a descriptor: {0}")] + DescriptorConversion(#[from] elements_miniscript::descriptor::ConversionError), + + #[error("Failed to construct a wpkh address: {0}")] + WpkhAddressConstruction(#[from] elements_miniscript::Error), +} diff --git a/crates/sdk/src/signer/mod.rs b/crates/sdk/src/signer/mod.rs new file mode 100644 index 0000000..ae3f09a --- /dev/null +++ b/crates/sdk/src/signer/mod.rs @@ -0,0 +1,5 @@ +pub mod error; +pub mod signer; + +pub use error::SignerError; +pub use signer::{Signer, SignerTrait}; diff --git a/crates/sdk/src/signer/signer.rs b/crates/sdk/src/signer/signer.rs new file mode 100644 index 0000000..a485dd8 --- /dev/null +++ b/crates/sdk/src/signer/signer.rs @@ -0,0 +1,401 @@ +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; + +use elements_miniscript::Descriptor; +use elements_miniscript::bitcoin::PublicKey; +use elements_miniscript::descriptor::Wpkh; +use simplicityhl::Value; +use simplicityhl::WitnessValues; +use simplicityhl::elements::pset::PartiallySignedTransaction; +use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, ecdsa, schnorr}; +use simplicityhl::elements::{Address, OutPoint, Script, Transaction, TxOut}; +use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; +use simplicityhl::simplicity::hashes::Hash; +use simplicityhl::str::WitnessName; +use simplicityhl::value::ValueConstructible; + +use bip39::Mnemonic; + +use elements_miniscript::{ + DescriptorPublicKey, + bitcoin::{NetworkKind, PrivateKey, bip32::DerivationPath}, + elements::{ + EcdsaSighashType, + bitcoin::bip32::{Fingerprint, Xpriv, Xpub}, + sighash::SighashCache, + }, + elementssig_to_rawsig, + psbt::PsbtExt, +}; + +use super::error::SignerError; +use crate::constants::{MIN_FEE, PLACEHOLDER_FEE, SimplicityNetwork}; +use crate::program::ProgramTrait; +use crate::provider::ProviderTrait; +use crate::transaction::FinalTransaction; +use crate::transaction::PartialInput; +use crate::transaction::PartialOutput; +use crate::transaction::RequiredSignature; + +pub trait SignerTrait { + fn sign_program( + &self, + pst: &PartiallySignedTransaction, + program: &Box, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result; + + fn sign_input( + &self, + pst: &PartiallySignedTransaction, + input_index: usize, + ) -> Result<(PublicKey, ecdsa::Signature), SignerError>; +} + +pub struct Signer { + xprv: Xpriv, + provider: Box, + network: SimplicityNetwork, + secp: Secp256k1, +} + +impl SignerTrait for Signer { + fn sign_program( + &self, + pst: &PartiallySignedTransaction, + program: &Box, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result { + let env = program.get_env(&pst, input_index, network)?; + let msg = Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); + + let private_key = self.get_private_key()?; + let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); + + Ok(self.secp.sign_schnorr(&msg, &keypair)) + } + + fn sign_input( + &self, + pst: &PartiallySignedTransaction, + input_index: usize, + ) -> Result<(PublicKey, ecdsa::Signature), SignerError> { + let tx = pst.extract_tx()?; + + let mut sighash_cache = SighashCache::new(&tx); + let genesis_hash = elements_miniscript::elements::BlockHash::all_zeros(); + + let message = pst + .sighash_msg(input_index, &mut sighash_cache, None, genesis_hash)? + .to_secp_msg(); + + let private_key = self.get_private_key()?; + let public_key = private_key.public_key(&self.secp); + + let signature = self.secp.sign_ecdsa_low_r(&message, &private_key.inner); + + Ok((public_key, signature)) + } +} + +enum Estimate { + Success(Transaction, u64), + Failure(u64), +} + +impl Signer { + pub fn new( + mnemonic: &str, + provider: Box, + network: SimplicityNetwork, + ) -> Result { + let secp = Secp256k1::new(); + let mnemonic: Mnemonic = mnemonic + .parse() + .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string()))?; + let seed = mnemonic.to_seed(""); + + let xprv = Xpriv::new_master(NetworkKind::Test, &seed)?; + + Ok(Self { + xprv, + provider: provider, + network: network, + secp: secp, + }) + } + + pub fn finalize(&self, tx: &FinalTransaction, target_blocks: u32) -> Result<(Transaction, u64), SignerError> { + let mut signer_utxos = self.get_wpkh_utxos()?; + let mut set = HashSet::new(); + + for input in tx.inputs() { + set.insert(OutPoint { + txid: input.partial_input.witness_txid.clone(), + vout: input.partial_input.witness_output_index, + }); + } + + signer_utxos + .retain(|utxo| utxo.1.asset.explicit().unwrap() == self.network.policy_asset() && !set.contains(&utxo.0)); + signer_utxos.sort_by(|a, b| b.1.value.cmp(&a.1.value)); + + let mut fee_tx = tx.clone(); + let mut curr_fee = MIN_FEE; + let fee_rate = self.provider.get_fee_rate(target_blocks)?; + + for utxo in signer_utxos { + let policy_amount_delta = fee_tx.calculate_fee_delta(); + + if policy_amount_delta >= curr_fee as i64 { + match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta as u64)? { + Estimate::Success(tx, fee) => return Ok((tx, fee)), + Estimate::Failure(required_fee) => curr_fee = required_fee, + } + } + + fee_tx.add_input(PartialInput::new(utxo.0, utxo.1), RequiredSignature::NativeEcdsa)?; + } + + Err(SignerError::NotEnoughFunds(curr_fee)) + } + + pub fn finalize_strict( + &self, + tx: &FinalTransaction, + target_blocks: u32, + ) -> Result<(Transaction, u64), SignerError> { + let policy_amount_delta = tx.calculate_fee_delta(); + + if policy_amount_delta < MIN_FEE as i64 { + return Err(SignerError::DustAmount(policy_amount_delta)); + } + + let fee_rate = self.provider.get_fee_rate(target_blocks)?; + + // policy_amount_delta will be > 0 + match self.estimate_tx(tx.clone(), fee_rate, policy_amount_delta as u64)? { + Estimate::Success(tx, fee) => Ok((tx, fee)), + Estimate::Failure(required_fee) => Err(SignerError::NotEnoughFeeAmount(policy_amount_delta, required_fee)), + } + } + + pub fn get_wpkh_address(&self) -> Result { + let fingerprint = self.fingerprint()?; + let path = self.get_derivation_path()?; + let xpub = self.derive_xpub(&path)?; + + let desc = format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)"); + + let descriptor: Descriptor = + Descriptor::Wpkh(Wpkh::from_str(&desc).map_err(|e| SignerError::WpkhDescriptor(e.to_string()))?); + + Ok(descriptor.clone().into_single_descriptors()?[0] + .at_derivation_index(1)? + .address(self.network.address_params())?) + } + + pub fn get_wpkh_utxos(&self) -> Result, SignerError> { + Ok(self.provider.fetch_address_utxos(&self.get_wpkh_address()?)?) + } + + pub fn get_schnorr_public_key(&self) -> Result { + let private_key = self.get_private_key()?; + let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner); + + Ok(keypair.x_only_public_key().0) + } + + pub fn get_ecdsa_public_key(&self) -> Result { + Ok(self.get_private_key()?.public_key(&self.secp)) + } + + pub fn get_private_key(&self) -> Result { + let master_xprv = self.master_xpriv()?; + let full_path = self.get_derivation_path()?; + + let derived = + full_path.extend(DerivationPath::from_str("0/1").map_err(|e| SignerError::DerivationPath(e.to_string()))?); + + let ext_derived = master_xprv.derive_priv(&self.secp, &derived)?; + + Ok(PrivateKey::new(ext_derived.private_key, NetworkKind::Test)) + } + + fn estimate_tx( + &self, + mut fee_tx: FinalTransaction, + fee_rate: f32, + available_delta: u64, + ) -> Result { + // estimate the tx fee with the change + // use this wpkh address as a change script + fee_tx.add_output(PartialOutput::new( + self.get_wpkh_address()?.script_pubkey(), + PLACEHOLDER_FEE, + self.network.policy_asset(), + )); + + fee_tx.add_output(PartialOutput::new( + Script::new(), + PLACEHOLDER_FEE, + self.network.policy_asset(), + )); + + let final_tx = self.sign_tx(&fee_tx)?; + let fee = fee_tx.calculate_fee(final_tx.weight(), fee_rate); + + if available_delta > fee && available_delta - fee >= MIN_FEE { + // we have enough funds to cover the change UTXO + let outputs = fee_tx.outputs_mut(); + + outputs[outputs.len() - 2].amount = available_delta - fee; + outputs[outputs.len() - 1].amount = fee; + + let final_tx = self.sign_tx(&fee_tx)?; + + return Ok(Estimate::Success(final_tx, fee)); + } + + // not enough funds, so we need to estimate without the change + fee_tx.remove_output(fee_tx.n_outputs() - 2); + + let final_tx = self.sign_tx(&fee_tx)?; + let fee = fee_tx.calculate_fee(final_tx.weight(), fee_rate); + + if available_delta < fee { + return Ok(Estimate::Failure(fee)); + } + + let outputs = fee_tx.outputs_mut(); + + // change the fee output amount + outputs[outputs.len() - 1].amount = available_delta; + + // finalize the tx with fee and without the change + let final_tx = self.sign_tx(&fee_tx)?; + + return Ok(Estimate::Success(final_tx, fee)); + } + + fn sign_tx(&self, tx: &FinalTransaction) -> Result { + let mut pst = tx.extract_pst(); + let inputs = tx.inputs(); + + for index in 0..inputs.len() { + let input = inputs[index].clone(); + + // we need to prune the program + if input.program_input.is_some() { + let program = input.program_input.unwrap(); + let signed_witness: Result = match input.required_sig { + // sign the program and insert the signature into the witness + RequiredSignature::Witness(witness_name) => Ok(self.get_signed_program_witness( + &pst, + &program.program, + &program.witness.build_witness(), + &witness_name, + index, + )?), + // just build the passed witness + _ => Ok(program.witness.build_witness()), + }; + let pruned_witness = program + .program + .finalize(&pst, &signed_witness.unwrap(), index, &self.network) + .unwrap(); + + pst.inputs_mut()[index].final_script_witness = Some(pruned_witness); + } else { + // we need to sign the UTXO as is + // TODO: do we always sign? + let signed_witness = self.sign_input(&pst, index)?; + let raw_sig = elementssig_to_rawsig(&(signed_witness.1, EcdsaSighashType::All)); + + pst.inputs_mut()[index].final_script_witness = Some(vec![raw_sig, signed_witness.0.to_bytes()]); + } + } + + Ok(pst.extract_tx()?) + } + + fn get_signed_program_witness( + &self, + pst: &PartiallySignedTransaction, + program: &Box, + witness: &WitnessValues, + witness_name: &String, + index: usize, + ) -> Result { + let signature = self.sign_program(pst, program, index, &self.network)?; + + let mut hm = HashMap::new(); + + witness.iter().for_each(|el| { + hm.insert(el.0.clone(), el.1.clone()); + }); + + hm.insert( + WitnessName::from_str_unchecked(witness_name.as_str()), + Value::byte_array(signature.serialize()), + ); + + Ok(WitnessValues::from(hm)) + } + + fn derive_xpriv(&self, path: &DerivationPath) -> Result { + Ok(self.xprv.derive_priv(&self.secp, &path)?) + } + + fn master_xpriv(&self) -> Result { + Ok(self.derive_xpriv(&DerivationPath::master())?) + } + + fn derive_xpub(&self, path: &DerivationPath) -> Result { + let derived = self.derive_xpriv(path)?; + + Ok(Xpub::from_priv(&self.secp, &derived)) + } + + fn master_xpub(&self) -> Result { + Ok(self.derive_xpub(&DerivationPath::master())?) + } + + fn fingerprint(&self) -> Result { + Ok(self.master_xpub()?.fingerprint()) + } + + fn get_derivation_path(&self) -> Result { + let coin_type = if self.network.is_mainnet() { 1776 } else { 1 }; + let path = format!("84h/{coin_type}h/0h"); + + Ok(DerivationPath::from_str(&format!("m/{path}")).map_err(|e| SignerError::DerivationPath(e.to_string()))?) + } +} + +#[cfg(test)] +mod tests { + use crate::provider::EsploraProvider; + + use super::*; + + #[test] + fn keys_correspond_to_address() { + let provider = EsploraProvider::new("https://blockstream.info/liquidtestnet/api".to_string()); + let signer = Signer::new( + "exist carry drive collect lend cereal occur much tiger just involve mean", + Box::new(provider.clone()), + SimplicityNetwork::LiquidTestnet, + ) + .unwrap(); + + let address = signer.get_wpkh_address().unwrap(); + let pubkey = signer.get_ecdsa_public_key().unwrap(); + + let derived_addr = Address::p2wpkh(&pubkey, None, SimplicityNetwork::LiquidTestnet.address_params()); + + assert_eq!(derived_addr.to_string(), address.to_string()); + } +} diff --git a/crates/sdk/src/transaction/error.rs b/crates/sdk/src/transaction/error.rs new file mode 100644 index 0000000..1591dfc --- /dev/null +++ b/crates/sdk/src/transaction/error.rs @@ -0,0 +1,5 @@ +#[derive(Debug, thiserror::Error)] +pub enum TransactionError { + #[error("Invalid signature type requested: {0}")] + SignatureRequest(String), +} diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs new file mode 100644 index 0000000..9215a8d --- /dev/null +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -0,0 +1,159 @@ +use simplicityhl::elements::pset::PartiallySignedTransaction; + +use crate::constants::{SimplicityNetwork, WITNESS_SCALE_FACTOR}; + +use super::error::TransactionError; +use super::partial_input::{PartialInput, ProgramInput, RequiredSignature}; +use super::partial_output::PartialOutput; + +#[derive(Clone)] +pub struct FinalInput { + pub partial_input: PartialInput, + pub program_input: Option, + pub required_sig: RequiredSignature, +} + +#[derive(Clone)] +pub struct FinalTransaction { + pub network: SimplicityNetwork, + inputs: Vec, + outputs: Vec, +} + +impl FinalTransaction { + pub fn new(network: SimplicityNetwork) -> Self { + Self { + network: network, + inputs: Vec::new(), + outputs: Vec::new(), + } + } + + // TODO: require required_sig != Witness(String) + pub fn add_input( + &mut self, + partial_input: PartialInput, + required_sig: RequiredSignature, + ) -> Result<(), TransactionError> { + match required_sig { + RequiredSignature::Witness(_) => { + return Err(TransactionError::SignatureRequest( + "Requested signature is not NativeEcdsa or None".to_string(), + )); + } + _ => {} + } + + self.inputs.push(FinalInput { + partial_input: partial_input, + program_input: None, + required_sig: required_sig, + }); + + Ok(()) + } + + pub fn add_program_input( + &mut self, + partial_input: PartialInput, + program_input: ProgramInput, + required_sig: RequiredSignature, + ) -> Result<(), TransactionError> { + match required_sig { + RequiredSignature::NativeEcdsa => { + return Err(TransactionError::SignatureRequest( + "Requested signature is not Witness or None".to_string(), + )); + } + _ => {} + } + + self.inputs.push(FinalInput { + partial_input: partial_input, + program_input: Some(program_input), + required_sig: required_sig, + }); + + Ok(()) + } + + pub fn remove_input(&mut self, index: usize) -> Option { + if self.inputs.get(index).is_some() { + return Some(self.inputs.remove(index)); + } + + None + } + + pub fn add_output(&mut self, partial_output: PartialOutput) { + self.outputs.push(partial_output); + } + + pub fn remove_output(&mut self, index: usize) -> Option { + if self.outputs.get(index).is_some() { + return Some(self.outputs.remove(index)); + } + + None + } + + pub fn inputs(&self) -> &[FinalInput] { + &self.inputs + } + + pub fn inputs_mut(&mut self) -> &mut [FinalInput] { + &mut self.inputs + } + + pub fn outputs(&self) -> &[PartialOutput] { + &self.outputs + } + + pub fn outputs_mut(&mut self) -> &mut [PartialOutput] { + &mut self.outputs + } + + pub fn n_inputs(&self) -> usize { + self.inputs.len() + } + + pub fn n_outputs(&self) -> usize { + self.outputs.len() + } + + pub fn calculate_fee_delta(&self) -> i64 { + let available_amount = self + .inputs + .iter() + .filter(|input| input.partial_input.asset.clone().unwrap() == self.network.policy_asset()) + .fold(0 as u64, |acc, input| acc + input.partial_input.amount.clone().unwrap()); + + let consumed_amount = self + .outputs + .iter() + .filter(|output| output.asset == self.network.policy_asset()) + .fold(0 as u64, |acc, output| acc + output.amount); + + available_amount as i64 - consumed_amount as i64 + } + + pub 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 + } + + pub fn extract_pst(&self) -> PartiallySignedTransaction { + let mut pst = PartiallySignedTransaction::new_v2(); + + self.inputs.iter().for_each(|el| { + pst.add_input(el.partial_input.to_input()); + }); + + self.outputs.iter().for_each(|el| { + pst.add_output(el.to_output()); + }); + + pst + } +} diff --git a/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs new file mode 100644 index 0000000..41c0ba3 --- /dev/null +++ b/crates/sdk/src/transaction/mod.rs @@ -0,0 +1,9 @@ +pub mod error; +pub mod final_transaction; +pub mod partial_input; +pub mod partial_output; + +pub use error::TransactionError; +pub use final_transaction::{FinalInput, FinalTransaction}; +pub use partial_input::{PartialInput, ProgramInput, RequiredSignature}; +pub use partial_output::PartialOutput; diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs new file mode 100644 index 0000000..ef45e8d --- /dev/null +++ b/crates/sdk/src/transaction/partial_input.rs @@ -0,0 +1,73 @@ +use simplicityhl::elements::confidential::{Asset, Value}; +use simplicityhl::elements::pset::Input; +use simplicityhl::elements::{AssetId, OutPoint, Sequence, TxOut, Txid}; + +use crate::program::ProgramTrait; +use crate::program::WitnessTrait; + +#[derive(Clone)] +pub enum RequiredSignature { + None, + NativeEcdsa, + Witness(String), +} + +#[derive(Clone)] +pub struct PartialInput { + pub witness_txid: Txid, + pub witness_output_index: u32, + pub witness_utxo: TxOut, + pub sequence: Option, + pub amount: Option, + pub asset: Option, +} + +#[derive(Clone)] +pub struct ProgramInput { + pub program: Box, + pub witness: Box, +} + +impl PartialInput { + pub fn new(outpoint: OutPoint, txout: TxOut) -> Self { + let amount = match txout.value { + Value::Explicit(value) => Some(value), + _ => None, + }; + let asset = match txout.asset { + Asset::Explicit(asset) => Some(asset), + _ => None, + }; + + Self { + witness_txid: outpoint.txid, + witness_output_index: outpoint.vout, + witness_utxo: txout, + sequence: Default::default(), + amount: amount, + asset: asset, + } + } + + pub fn to_input(&self) -> Input { + let mut input = Input::default(); + + input.previous_txid = self.witness_txid.clone(); + input.previous_output_index = self.witness_output_index; + input.witness_utxo = Some(self.witness_utxo.clone()); + input.sequence = self.sequence.clone(); + input.amount = self.amount.clone(); + input.asset = self.asset.clone(); + + input + } +} + +impl ProgramInput { + pub fn new(program: Box, witness: Box) -> Self { + Self { + program: program, + witness: witness, + } + } +} diff --git a/crates/sdk/src/transaction/partial_output.rs b/crates/sdk/src/transaction/partial_output.rs new file mode 100644 index 0000000..27235bb --- /dev/null +++ b/crates/sdk/src/transaction/partial_output.rs @@ -0,0 +1,23 @@ +use simplicityhl::elements::pset::Output; +use simplicityhl::elements::{AssetId, Script}; + +#[derive(Clone)] +pub struct PartialOutput { + pub script_pubkey: Script, + pub amount: u64, + pub asset: AssetId, +} + +impl PartialOutput { + pub fn new(script: Script, amount: u64, asset: AssetId) -> Self { + Self { + script_pubkey: script, + amount: amount, + asset: asset, + } + } + + pub fn to_output(&self) -> Output { + Output::new_explicit(self.script_pubkey.clone(), self.amount, self.asset.clone(), None) + } +} diff --git a/crates/sdk/src/witness.rs b/crates/sdk/src/witness.rs deleted file mode 100644 index 7e58e40..0000000 --- a/crates/sdk/src/witness.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub trait WitnessTrait { - fn build_witness(&self) -> simplicityhl::WitnessValues; -} diff --git a/crates/sdk/src/witness_transaction.rs b/crates/sdk/src/witness_transaction.rs deleted file mode 100644 index 3f4b58d..0000000 --- a/crates/sdk/src/witness_transaction.rs +++ /dev/null @@ -1,254 +0,0 @@ -use simplicityhl::WitnessValues; -use simplicityhl::elements::pset::{Output, PartiallySignedTransaction}; -use simplicityhl::elements::secp256k1_zkp::schnorr::Signature; -use simplicityhl::elements::{Script, Transaction, TxOut}; - -use crate::constants::{MIN_FEE, PLACEHOLDER_FEE, SimplicityNetwork, WITNESS_SCALE_FACTOR}; -use crate::error::SimplexError; -use crate::program::ProgramTrait; -use crate::provider::ProviderSync; -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 WitnessTransaction<'a, T> { - pst: PartiallySignedTransaction, - network: SimplicityNetwork, - inputs: Vec>, -} - -impl<'a, T> WitnessTransaction<'a, T> -where - T: Fn(&WitnessValues, &Signature) -> Result + Clone, -{ - pub fn new(pst: PartiallySignedTransaction, network: SimplicityNetwork) -> Self { - Self { - pst: pst, - network: network, - inputs: Vec::new(), - } - } - - pub fn add_input(&mut self, program: &'a dyn ProgramTrait, witness: &'a dyn WitnessTrait) { - let signed_input = SignedInput { - program: program, - witness: witness, - signer: Option::None, - signer_lambda: Option::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: program, - witness: witness, - signer: Option::Some(signer), - signer_lambda: Option::Some(signer_lambda), - }; - - self.inputs.push(signed_input); - } - - pub fn finalize_with_fee( - &self, - target_blocks: u32, - change_recipient_script: Script, - provider: impl ProviderSync, - ) -> Result<(Transaction, u64), SimplexError> { - let policy_amount_delta = self.calculate_fee_delta(); - - if policy_amount_delta < MIN_FEE { - return Err(SimplexError::DustAmount(policy_amount_delta)); - } - - // estimate the tx fee with the change - let fee_rate = provider.get_fee_rate(target_blocks)?; - let mut fee_pst = self.pst.clone(); - - fee_pst.add_output(Output::new_explicit( - change_recipient_script.clone(), - PLACEHOLDER_FEE, - self.network.policy_asset(), - None, - )); - - fee_pst.add_output(Output::new_explicit( - Script::new(), - PLACEHOLDER_FEE, - self.network.policy_asset(), - None, - )); - - let (final_tx, utxos) = self.extract_tx_and_utxos(&fee_pst)?; - let final_tx = self.finalize_tx(final_tx, utxos.as_slice())?; - let fee = self.calculate_fee(final_tx.weight(), fee_rate); - - if policy_amount_delta > fee && policy_amount_delta - fee >= MIN_FEE { - // we have enough funds to cover change UTXO - let mut fee_pst = self.pst.clone(); - - fee_pst.add_output(Output::new_explicit( - change_recipient_script, - policy_amount_delta - fee, - self.network.policy_asset(), - None, - )); - - fee_pst.add_output(Output::new_explicit( - Script::new(), - fee, - self.network.policy_asset(), - None, - )); - - let (final_tx, utxos) = self.extract_tx_and_utxos(&fee_pst)?; - let final_tx = self.finalize_tx(final_tx, utxos.as_slice())?; - - return Ok((final_tx, fee)); - } - - // not enough funds, so we need to estimate without the change - let mut fee_pst = self.pst.clone(); - - fee_pst.add_output(Output::new_explicit( - Script::new(), - PLACEHOLDER_FEE, - self.network.policy_asset(), - None, - )); - - let (final_tx, utxos) = self.extract_tx_and_utxos(&fee_pst)?; - let final_tx = self.finalize_tx(final_tx, utxos.as_slice())?; - let fee = self.calculate_fee(final_tx.weight(), fee_rate); - - // policy amount is not exact - if policy_amount_delta != fee { - return Err(SimplexError::NotEnoughFeeAmount(policy_amount_delta, fee)); - } - - // finalize the tx with fee and without the change - let mut fee_pst = self.pst.clone(); - - fee_pst.add_output(Output::new_explicit( - Script::new(), - fee, - self.network.policy_asset(), - None, - )); - - let (final_tx, utxos) = self.extract_tx_and_utxos(&fee_pst)?; - let final_tx = self.finalize_tx(final_tx, utxos.as_slice())?; - - Ok((final_tx, fee)) - } - - pub fn finalize(&self) -> Result { - let (final_tx, utxos) = self.extract_tx_and_utxos(&self.pst)?; - - Ok(self.finalize_tx(final_tx, utxos.as_slice())?) - } - - fn finalize_tx(&self, mut final_tx: Transaction, utxos: &[TxOut]) -> Result { - 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_tx_with_signer( - final_tx, - utxos, - program, - witness.build_witness(), - index, - signer.unwrap(), - signer_lambda.unwrap(), - )?; - } else { - final_tx = self.finalize_tx_as_is(final_tx, utxos, program, witness.build_witness(), index)?; - } - } - - Ok(final_tx) - } - - #[allow(clippy::too_many_arguments)] - fn finalize_tx_with_signer( - &self, - final_tx: Transaction, - utxos: &[TxOut], - program: &dyn ProgramTrait, - witness: WitnessValues, - index: usize, - signer: &dyn SignerTrait, - signer_lambda: T, - ) -> Result { - let signature = signer.sign(program, &final_tx, utxos, index, self.network)?; - let new_witness = signer_lambda(&witness, &signature)?; - - self.finalize_tx_as_is(final_tx, utxos, program, new_witness, index) - } - - fn finalize_tx_as_is( - &self, - final_tx: Transaction, - utxos: &[TxOut], - program: &dyn ProgramTrait, - witness: WitnessValues, - index: usize, - ) -> Result { - program.finalize(witness, final_tx, utxos, index, self.network) - } - - fn calculate_fee_delta(&self) -> u64 { - let available_amount = self - .pst - .inputs() - .iter() - .filter(|input| input.asset.unwrap() == self.network.policy_asset()) - .fold(0_u64, |acc, input| acc + input.amount.unwrap()); - - let consumed_amount = self - .pst - .outputs() - .iter() - .filter(|output| output.asset.unwrap() == self.network.policy_asset()) - .fold(0_u64, |acc, output| acc + output.amount.unwrap()); - - available_amount - consumed_amount - } - - 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 - } - - fn extract_tx_and_utxos( - &self, - pst: &PartiallySignedTransaction, - ) -> Result<(Transaction, Vec), SimplexError> { - let final_tx = pst.extract_tx()?; - let mut utxos: Vec = vec![]; - - for input in pst.inputs() { - utxos.push(input.witness_utxo.clone().unwrap()); - } - - Ok((final_tx, utxos)) - } -} diff --git a/crates/simplex/README.md b/crates/simplex/README.md deleted file mode 100644 index caf7a9d..0000000 --- a/crates/simplex/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Simpelex HL Core - -This crate provides useful utilities for working with Simplicity on Elements. - -- `blinder.rs` — derives deterministic blinder keypair from a "public secret" -- `constants.rs` — Liquid network constants (policy asset IDs, genesis hashes) -- `explorer.rs` — explorer API utilities (behind `explorer` feature) -- `runner.rs` — program execution helpers with logging -- `scripts.rs` — P2TR address creation, Taproot control block, and asset entropy utilities -- `lib.rs` — P2PK program helpers and transaction finalization - -Consider this more like a test helper tool rather than a production-ready version. - -## License - -Dual-licensed under either of: -- Apache License, Version 2.0 (Apache-2.0) -- MIT license (MIT) - -at your option. diff --git a/crates/simplex/examples/example.rs b/crates/simplex/examples/example.rs deleted file mode 100644 index 58d1864..0000000 --- a/crates/simplex/examples/example.rs +++ /dev/null @@ -1,35 +0,0 @@ -use simplex_macros::include_simf; - -include_simf!("examples/source_simf/p2pk.simf"); - -fn main() -> Result<(), String> { - // let original_witness = derived_options::OptionsWitness { - // path: simplicityhl::either::Either::Right(simplicityhl::either::Either::Left((true, 100, 200))), - // }; - - // let witness_values = original_witness.build_witness(); - // let recovered_witness = derived_options::OptionsWitness::from_witness(&witness_values)?; - // assert_eq!(original_witness, recovered_witness); - - // let original_arguments = derived_options::OptionsArguments { - // start_time: 0, - // expiry_time: 0, - // grantor_reissuance_token_asset: Default::default(), - // grantor_token_asset: Default::default(), - // settlement_per_contract: Default::default(), - // settlement_asset_id: Default::default(), - // collateral_per_contract: Default::default(), - // collateral_asset_id: Default::default(), - // option_reissuance_token_asset: Default::default(), - // option_token_asset: Default::default(), - // }; - - // let witness_values = original_arguments.build_arguments(); - // let recovered_witness = derived_options::OptionsArguments::from_arguments(&witness_values)?; - // assert_eq!(original_arguments, recovered_witness); - - // let _template = derived_options::get_template_program(); - // let _compiled = derived_options::get_compiled_program(&original_arguments); - - Ok(()) -} diff --git a/crates/simplex/examples/source_simf/array_tr_storage.simf b/crates/simplex/examples/source_simf/array_tr_storage.simf deleted file mode 100644 index 4918cf3..0000000 --- a/crates/simplex/examples/source_simf/array_tr_storage.simf +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Extends `bytes32_tr_storage` using `array_fold` for larger buffers. - * Optimized for small, fixed-size states where linear hashing is more efficient - * than Merkle Trees. By avoiding proof overhead like sibling hashes, we reduce - * witness size and simplify contract logic for small N. - * This approach is particularly advantageous when updating all slots within every transaction. - */ - -fn hash_array_tr_storage(elem: u256, ctx: Ctx8) -> Ctx8 { - jet::sha_256_ctx_8_add_32(ctx, elem) -} - -fn hash_array_tr_storage_with_update(elem: u256, triplet: (Ctx8, u16, u16)) -> (Ctx8, u16, u16) { - let (ctx, i, changed_index): (Ctx8, u16, u16) = triplet; - - match jet::eq_16(i, changed_index) { - true => { - let (_, val): (bool, u16) = jet::increment_16(i); - - // There may be arbitrary logic here - let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(elem); - let new_state4: u64 = 20; - - let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); - ( - jet::sha_256_ctx_8_add_32(ctx, new_state), - val, - changed_index, - ) - }, - false => { - let (_, val): (bool, u16) = jet::increment_16(i); - ( - jet::sha_256_ctx_8_add_32(ctx, elem), - val, - changed_index, - ) - } - } -} - -fn script_hash_for_input_script(state: [u256; 3], changed_index: Option) -> u256 { - let tap_leaf: u256 = jet::tapleaf_hash(); - let ctx: Ctx8 = jet::tapdata_init(); - - let (ctx, _, _): (Ctx8, u16, u16) = match changed_index { - Some(ind: u16) => { - array_fold::(state, (ctx, 0, ind)) - }, - None => { - (array_fold::(state, ctx), 0, 0) - } - }; - - let computed: u256 = jet::sha_256_ctx_8_finalize(ctx); - let tap_node: u256 = jet::build_tapbranch(tap_leaf, computed); - - let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; - let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); - - let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); - let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 - let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); - jet::sha_256_ctx_8_finalize(hash_ctx3) -} - -fn main() { - let state: [u256; 3] = witness::STATE; - - // Assert that the input is correct, i.e. "load". - assert!(jet::eq_256( - script_hash_for_input_script(state, None), - unwrap(jet::input_script_hash(jet::current_index())) - )); - - // Assert that the output is correct, i.e. "store". - assert!(jet::eq_256( - script_hash_for_input_script(state, Some(witness::CHANGED_INDEX)), - unwrap(jet::output_script_hash(jet::current_index())) - )); -} \ No newline at end of file diff --git a/crates/simplex/examples/source_simf/bytes32_tr_storage.simf b/crates/simplex/examples/source_simf/bytes32_tr_storage.simf deleted file mode 100644 index 0d11b5f..0000000 --- a/crates/simplex/examples/source_simf/bytes32_tr_storage.simf +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Computes the "State Commitment" — the expected Script PubKey (address) - * for a specific state value. - * - * HOW IT WORKS: - * In Simplicity/Liquid, state is not stored in a dedicated database. Instead, - * it is verified via a "Commitment Scheme" inside the Taproot tree of the UTXO. - * - * This function reconstructs the Taproot structure to validate that the provided - * witness data (state_data) was indeed cryptographically embedded into the - * transaction output that is currently being spent. - * - * LOGIC FLOW: - * 1. Takes state_data (passed via witness at runtime). - * 2. Hashes it as a non-executable TapData leaf. - * 3. Combines it with the current program's CMR (tapleaf_hash). - * 4. Derives the tweaked_key (Internal Key + Merkle Root). - * 5. Returns the final SHA256 script hash (SegWit v1). - * - * USAGE: - * - In main, we verify: CalculatedHash(witness::STATE) == input_script_hash. - * - This assertion proves that the UTXO is "locked" not just by the code, - * but specifically by THIS instance of the state data. - */ - -fn script_hash_for_input_script(state_data: u256) -> u256 { - // This is the bulk of our "compute state commitment" logic from above. - let tap_leaf: u256 = jet::tapleaf_hash(); - let state_ctx1: Ctx8 = jet::tapdata_init(); - let state_ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(state_ctx1, state_data); - let state_leaf: u256 = jet::sha_256_ctx_8_finalize(state_ctx2); - let tap_node: u256 = jet::build_tapbranch(tap_leaf, state_leaf); - - // Compute a taptweak using this. - let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; - let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); - - // Turn the taptweak into a script hash - let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); - let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 - let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); - jet::sha_256_ctx_8_finalize(hash_ctx3) -} - -fn main() { - let state_data: u256 = witness::STATE; - let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(state_data); - - // Assert that the input is correct, i.e. "load". - assert!(jet::eq_256( - script_hash_for_input_script(state_data), - unwrap(jet::input_script_hash(jet::current_index())) - )); - - // Do a state update (and fail on 64-bit overflow even though we've got 192 other - // bits we could be using..) - let (carry, new_state4): (bool, u64) = jet::increment_64(state4); - assert!(jet::eq_1(::into(carry), 0)); - - let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); - // Assert that the output is correct, i.e. "store". - assert!(jet::eq_256( - script_hash_for_input_script(new_state), - unwrap(jet::output_script_hash(jet::current_index())) - )); -} \ No newline at end of file diff --git a/crates/simplex/examples/source_simf/dual_currency_deposit.simf b/crates/simplex/examples/source_simf/dual_currency_deposit.simf deleted file mode 100644 index e1a460a..0000000 --- a/crates/simplex/examples/source_simf/dual_currency_deposit.simf +++ /dev/null @@ -1,592 +0,0 @@ -/* - * DCD: Dual Currency Deposit – price-attested settlement and funding windows - * - * Flows implemented: - * - Maker funding: deposit settlement asset and collateral, issue grantor tokens - * - Taker funding: deposit collateral in window and receive filler tokens - * - Settlement: at SETTLEMENT_HEIGHT, oracle Schnorr signature over (height, price) - * selects LBTC vs ALT branch based on price <= STRIKE_PRICE - * - Early/post-expiry termination: taker returns filler; maker burns grantor tokens - * - Merge: consolidate 2/3/4 token UTXOs - * - * All amounts and asset/script invariants are enforced on-chain; time guards use - * fallback locktime and height checks. - * - * Batching discussion: https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 - */ - -// Verify Schnorr signature against SHA256 of (u32 || u64) -fn checksig_priceblock(pk: Pubkey, current_block_height: u32, price_at_current_block_height: u64, sig: Signature) { - let hasher: Ctx8 = jet::sha_256_ctx_8_init(); - let hasher: Ctx8 = jet::sha_256_ctx_8_add_4(hasher, current_block_height); - let hasher: Ctx8 = jet::sha_256_ctx_8_add_8(hasher, price_at_current_block_height); - let msg: u256 = jet::sha_256_ctx_8_finalize(hasher); - jet::bip_0340_verify((pk, msg), sig); -} - -// Signed <= using XOR with 0x8000.. bias: a<=b (signed) iff (a^bias) <= (b^bias) (unsigned) -fn signed_le_u64(a_bits: u64, b_bits: u64) -> bool { - let bias: u64 = 0x8000000000000000; - jet::le_64(jet::xor_64(a_bits, bias), jet::xor_64(b_bits, bias)) -} - -fn signed_lt_u64(a: u64, b: u64) -> bool { - let bias: u64 = 0x8000000000000000; - jet::lt_64(jet::xor_64(a, bias), jet::xor_64(b, bias)) -} - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -/// Assert: base_amount * basis_point_percentage == provided_amount * MAX_BASIS_POINTS -fn constraint_percentage(base_amount: u64, basis_point_percentage: u64, provided_amount: u64) { - let MAX_BASIS_POINTS: u64 = 10000; - - let arg1: u256 = <(u128, u128)>::into((0, jet::multiply_64(base_amount, basis_point_percentage))); - let arg2: u256 = <(u128, u128)>::into((0, jet::multiply_64(provided_amount, MAX_BASIS_POINTS))); - - assert!(jet::eq_256(arg1, arg2)); -} - -fn get_output_script_hash(index: u32) -> u256 { - unwrap(jet::output_script_hash(index)) -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } -fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } - -fn ensure_one_bit_or(bit1: bool, bit2: bool) { - assert!( - jet::eq_1( - ::into(jet::or_1(::into(bit1), ::into(bit2))), - 1 - ) - ); -} - -fn increment_by(index: u32, amount: u32) -> u32 { - let (carry, result): (bool, u32) = jet::add_32(index, amount); - ensure_zero_bit(carry); - result -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn ensure_output_is_op_return(index: u32) { - match jet::output_null_datum(index, 0) { - Some(entry: Option>>) => (), - None => panic!(), - } -} - -fn ensure_input_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::input_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::output_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_input_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - assert!(jet::eq_32(jet::current_index(), index)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -fn merge_2_tokens() { - // 2 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 3)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 1)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); -} - -fn merge_3_tokens() { - // 3 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 4)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 2)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); - assert!(jet::eq_256(script_hash, get_input_script_hash(2))); -} - -fn merge_4_tokens() { - // 4 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 5)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 3)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); - assert!(jet::eq_256(script_hash, get_input_script_hash(2))); - assert!(jet::eq_256(script_hash, get_input_script_hash(3))); -} - -/* -* Maker funding path -* Params: -* 1. FILLER_PER_SETTLEMENT_COLLATERAL -* 2. FILLER_PER_SETTLEMENT_ASSET -* 3. FILLER_PER_PRINCIPAL_COLLATERAL -* 4. GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET -* 5. GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL -* 6. GRANTOR_PER_SETTLEMENT_COLLATERAL -* 7. GRANTOR_PER_SETTLEMENT_ASSET -*/ -fn maker_funding_path(principal_collateral_amount: u64, principal_asset_amount: u64, interest_collateral_amount: u64, interest_asset_amount: u64) { - assert!(jet::eq_32(jet::num_inputs(), 5)); - assert!(jet::eq_32(jet::num_outputs(), 11)); - - let current_time: u32 = ::into(jet::lock_time()); - assert!(jet::lt_32(current_time, param::TAKER_FUNDING_START_TIME)); - - ensure_input_and_output_script_hash_eq(0); - ensure_input_and_output_script_hash_eq(1); - ensure_input_and_output_script_hash_eq(2); - - assert!(jet::le_32(jet::current_index(), 2)); - - let script_hash: u256 = get_output_script_hash(0); - ensure_output_script_hash_eq(1, script_hash); - ensure_output_script_hash_eq(2, script_hash); - ensure_output_script_hash_eq(3, script_hash); - ensure_output_script_hash_eq(4, script_hash); - ensure_output_script_hash_eq(5, script_hash); - - let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(3); - let (settlement_asset_bits, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(4); - let filler_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); - let grantor_collateral_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); - let grantor_settlement_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(2)))); - assert!(jet::eq_64(filler_token_amount, grantor_collateral_token_amount)); - assert!(jet::eq_64(filler_token_amount, grantor_settlement_token_amount)); - - divmod_eq(principal_asset_amount, param::STRIKE_PRICE, principal_collateral_amount); - - assert!(jet::eq_64(collateral_amount, interest_collateral_amount)); - constraint_percentage(principal_collateral_amount, param::INCENTIVE_BASIS_POINTS, collateral_amount); - - let MAX_BASIS_POINTS: u64 = 10000; - let (carry, asset_incentive_percentage): (bool, u64) = jet::add_64(param::INCENTIVE_BASIS_POINTS, MAX_BASIS_POINTS); - ensure_zero_bit(carry); - - constraint_percentage(principal_asset_amount, asset_incentive_percentage, settlement_amount); - - let (carry, calculated_total_asset_amount): (bool, u64) = jet::add_64(principal_asset_amount, interest_asset_amount); - ensure_zero_bit(carry); - assert!(jet::eq_64(calculated_total_asset_amount, settlement_amount)); - - let (carry, calculated_total_collateral_amount): (bool, u64) = jet::add_64(principal_collateral_amount, interest_collateral_amount); - ensure_zero_bit(carry); - - // Filler token constraints - divmod_eq(calculated_total_collateral_amount, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_token_amount); - divmod_eq(calculated_total_asset_amount, param::FILLER_PER_SETTLEMENT_ASSET, filler_token_amount); - divmod_eq(principal_collateral_amount, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount); - - // Grantor token constraints - divmod_eq(calculated_total_asset_amount, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_token_amount); - divmod_eq(interest_collateral_amount, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_token_amount); - - divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_collateral_token_amount); - // divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_settlement_token_amount); // duplicated because of lines 203-204 - - divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_collateral_token_amount); - // divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_settlement_token_amount); // duplicated because of lines 203-204 - - assert!(jet::eq_256(param::COLLATERAL_ASSET_ID, collateral_asset_bits)); - assert!(jet::eq_256(param::SETTLEMENT_ASSET_ID, settlement_asset_bits)); - - ensure_output_asset_with_amount_eq(5, param::FILLER_TOKEN_ASSET, filler_token_amount); - ensure_output_asset_with_amount_eq(6, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_token_amount); - ensure_output_asset_with_amount_eq(7, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_token_amount); - - ensure_input_asset_eq(3, param::SETTLEMENT_ASSET_ID); - ensure_input_asset_eq(4, param::COLLATERAL_ASSET_ID); - - ensure_output_asset_eq(8, param::COLLATERAL_ASSET_ID); - ensure_output_asset_eq(9, param::SETTLEMENT_ASSET_ID); - ensure_output_asset_eq(10, param::COLLATERAL_ASSET_ID); -} - -fn taker_funding_path(collateral_amount_to_deposit: u64, filler_token_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - assert!(jet::le_32(param::TAKER_FUNDING_START_TIME, current_time)); - assert!(jet::lt_32(current_time, param::TAKER_FUNDING_END_TIME)); - assert!(jet::lt_32(current_time, param::CONTRACT_EXPIRY_TIME)); - - let filler_token_input_index: u32 = 0; - let collateral_input_index: u32 = 1; - - let (collateral_to_covenant_output_index, filler_to_user_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(filler_token_input_index); - - // Check and ensure filler token change - ensure_correct_change_at_index(0, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_deposit, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_get); - - // Ensure collateral asset and script hash are correct - ensure_output_asset_with_amount_eq(collateral_to_covenant_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_deposit); - ensure_output_script_hash_eq(collateral_to_covenant_output_index, expected_current_script_hash); - - ensure_output_asset_with_amount_eq(filler_to_user_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get); -} - -fn taker_early_termination_path(filler_token_amount_to_return: u64, collateral_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let collateral_input_index: u32 = 0; - let filler_token_input_index: u32 = 1; - - let (return_filler_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_return); - - // Ensure filler token transferred to covenant - ensure_output_asset_with_amount_eq(return_filler_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_return); - ensure_output_script_hash_eq(return_filler_output_index, expected_current_script_hash); - - // Ensure collateral transferred to user - ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); -} - -fn maker_collateral_termination_path(grantor_collateral_amount_to_burn: u64, collateral_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let collateral_input_index: u32 = 0; - let grantor_collateral_token_input_index: u32 = 1; - - let (burn_grantor_collateral_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_amount_to_burn); - - // Burn grantor collateral token - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_amount_to_burn); - - // Ensure collateral transferred to user - ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); -} - -fn maker_settlement_termination_path(grantor_settlement_amount_to_burn: u64, settlement_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let settlement_asset_input_index: u32 = 0; - let grantor_settlement_token_input_index: u32 = 1; - - let (burn_grantor_settlement_output_index, return_settlement_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(settlement_amount_to_get, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_amount_to_burn); - - // Burn grantor settlement token - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_amount_to_burn); - - // Ensure settlement asset transferred to user - ensure_output_asset_with_amount_eq(return_settlement_output_index, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get); -} - -fn ensure_correct_return_at(user_output_index: u32, asset_id: u256, amount_to_get: u64, fee_basis_points: u64) { - match jet::eq_64(fee_basis_points, 0) { - true => ensure_output_asset_with_amount_eq(user_output_index, asset_id, amount_to_get), - false => { - let fee_output_index: u32 = increment_by(user_output_index, 1); - - let (user_asset_bits, user_amount): (u256, u64) = get_output_explicit_asset_amount(user_output_index); - assert!(jet::eq_256(user_asset_bits, asset_id)); - - let (fee_asset_bits, fee_amount): (u256, u64) = get_output_explicit_asset_amount(fee_output_index); - assert!(jet::eq_256(fee_asset_bits, asset_id)); - - let (carry, calculated_total_amount): (bool, u64) = jet::add_64(user_amount, fee_amount); - ensure_zero_bit(carry); - - constraint_percentage(calculated_total_amount, fee_basis_points, fee_amount); - - ensure_output_script_hash_eq(fee_output_index, param::FEE_SCRIPT_HASH); - }, - }; -} - -fn maker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, grantor_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { - jet::check_lock_height(param::SETTLEMENT_HEIGHT); - checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); - - match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { - true => { - // Maker gets ALT - let settlement_asset_input_index: u32 = 0; - - let (burn_grantor_settlement_output_index, burn_grantor_collateral_output_index, settlement_output_index): (u32, u32, u32) = match is_change_needed { - true => (1, 2, 3), - false => (0, 1, 2), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_amount_to_burn); - - // Burn grantor settlement and collateral tokens - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); - - // Ensure settlement asset transferred to user - ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - false => { - // Maker gets the LBTC - let collateral_input_index: u32 = 0; - - let (burn_grantor_collateral_output_index, burn_grantor_settlement_output_index, collateral_output_index): (u32, u32, u32) = match is_change_needed { - true => (1, 2, 3), - false => (0, 1, 2), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_amount_to_burn); - - // Burn grantor collateral and settlement tokens - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); - - // Ensure collateral transferred to user - ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - } -} - -fn taker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, filler_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { - jet::check_lock_height(param::SETTLEMENT_HEIGHT); - checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); - - match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { - true => { - // Taker receives LBTC principal+interest - let collateral_input_index: u32 = 0; - - let (burn_filler_output_index, collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_amount_to_burn); - - // Burn filler token - ensure_output_is_op_return(burn_filler_output_index); - ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); - - // Ensure collateral transferred to user - ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - false => { - // Taker receives ALT - let settlement_asset_input_index: u32 = 0; - - let (burn_filler_output_index, settlement_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_ASSET, filler_amount_to_burn); - - // Burn filler token - ensure_output_is_op_return(burn_filler_output_index); - ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); - - // Ensure filler token transferred to user - ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - } -} - -fn main() { - let token_branch: Either<(), ()> = witness::TOKEN_BRANCH; - let merge_branch: Either, ()> = witness::MERGE_BRANCH; - - match witness::PATH { - Left(funding_or_settlement: Either, (u64, Signature, u64, u64, bool)>) => match funding_or_settlement { - // Funding branches - Left(funding_params: Either<(u64, u64, u64, u64), (u64, u64, bool)>) => match funding_params { - // Maker funding: (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) - Left(params: (u64, u64, u64, u64)) => { - let (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount): (u64, u64, u64, u64) = params; - maker_funding_path(principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) - }, - // Taker funding: (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) - Right(params: (u64, u64, bool)) => { - let (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed): (u64, u64, bool) = params; - taker_funding_path(collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) - }, - }, - // Settlement branches (oracle price attested) - Right(params: (u64, Signature, u64, u64, bool)) => { - let (price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed): (u64, Signature, u64, u64, bool) = params; - - match token_branch { - // Maker settlement: burn grantor token - Left(u: ()) => maker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), - // Taker settlement: burn filler token - Right(u: ()) => taker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), - } - }, - }, - // Termination flows (early termination or post-expiry) or Merge flows - Right(termination_or_maker_or_merge: Either, ()>) => match termination_or_maker_or_merge { - Left(termination_or_maker: Either<(bool, u64, u64), (bool, u64, u64)>) => match termination_or_maker { - // Taker early termination: (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get) - Left(params: (bool, u64, u64)) => { - let (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get): (bool, u64, u64) = params; - taker_early_termination_path(filler_token_amount_to_return, collateral_amount_to_get, is_change_needed) - }, - // Maker termination (burn grantor token): choose collateral vs settlement token via token_branch - Right(params: (bool, u64, u64)) => { - let (is_change_needed, grantor_token_amount_to_burn, amount_to_get): (bool, u64, u64) = params; - - match token_branch { - // Burn grantor collateral token -> receive collateral - Left(u: ()) => maker_collateral_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), - // Burn grantor settlement token -> receive settlement asset - Right(u: ()) => maker_settlement_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), - } - }, - }, - Right(u: ()) => { - // Merge tokens based on MERGE_BRANCH discriminator - match merge_branch { - Left(left_or_right: Either<(), ()>) => match left_or_right { - Left(u: ()) => merge_2_tokens(), - Right(u: ()) => merge_3_tokens(), - }, - Right(u: ()) => merge_4_tokens(), - } - }, - }, - } - -} diff --git a/crates/simplex/examples/source_simf/option_offer.simf b/crates/simplex/examples/source_simf/option_offer.simf deleted file mode 100644 index 5cb2108..0000000 --- a/crates/simplex/examples/source_simf/option_offer.simf +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Option Offer - * - * A covenant that allows a user to deposit collateral and premium assets, - * and have a counterparty swap settlement asset for both. - * The user can withdraw accumulated settlement asset at any time (with signature). - * After expiry, the user can reclaim any remaining collateral and premium (with signature). - * - * Paths: - * 1. Exercise: Counterparty swaps settlement asset for collateral + premium (no time restriction, optional change) - * 2. Withdraw: User withdraws settlement asset (no time restriction, signature required, full amount) - * 3. Expiry: User reclaims collateral + premium (after expiry, signature required, full amount) - * - * Constraints: - * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount - * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount - */ - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_zero_bit(bit: bool) { - assert!(jet::eq_1(::into(bit), 0)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn check_user_signature(sig: Signature) { - let msg: u256 = jet::sig_all_hash(); - jet::bip_0340_verify((param::USER_PUBKEY, msg), sig); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, asset_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, asset_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -/* - * Exercise Path - * - * Counterparty swaps settlement asset for collateral + premium. - * No time restriction - works before and after expiry. - * - * Constraints: - * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount - * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount - * - * Layout: - * - * Both: - * Input[0]: Collateral from covenant - * Input[1]: Premium from covenant - * - * With change (partial swap): - * Output[0]: Collateral change → covenant - * Output[1]: Premium change → covenant - * Output[2]: Settlement asset → covenant - * Output[3]: Collateral → counterparty - * Output[4]: Premium → counterparty - * - * Without change (full swap): - * Output[0]: Settlement asset → covenant - * Output[1]: Collateral → counterparty - * Output[2]: Premium → counterparty - */ -fn exercise_path(collateral_amount: u64, is_change_needed: bool) { - assert!(jet::le_32(jet::current_index(), 1)); - - let expected_covenant_script_hash: u256 = get_input_script_hash(0); - - assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); - - let premium_amount_u128: u128 = jet::multiply_64(collateral_amount, param::PREMIUM_PER_COLLATERAL); - let (left_part, premium_amount): (u64, u64) = dbg!(::into(premium_amount_u128)); - assert!(jet::eq_64(left_part, 0)); - - // Check collateral changes - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_covenant_script_hash, is_change_needed); - ensure_correct_change_at_index(1, param::PREMIUM_ASSET_ID, premium_amount, expected_covenant_script_hash, is_change_needed); - - let (settlement_output_index, collateral_output_index, premium_output_index): (u32, u32, u32) = match is_change_needed { - true => (2, 3, 4), - false => (0, 1, 2), - }; - - ensure_output_script_hash_eq(settlement_output_index, expected_covenant_script_hash); - - let (output_asset, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(settlement_output_index); - assert!(jet::eq_256(output_asset, param::SETTLEMENT_ASSET_ID)); - - divmod_eq(settlement_amount, param::COLLATERAL_PER_CONTRACT, collateral_amount); - - ensure_output_asset_with_amount_eq(collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount); - ensure_output_asset_with_amount_eq(premium_output_index, param::PREMIUM_ASSET_ID, premium_amount); -} - -/* - * Withdraw Path - * - * User withdraws accumulated settlement asset. - * No time restriction. - * Requires signature from USER_PUBKEY. - * No change - full withdrawal only. - * - * Layout: - * Input[0]: Settlement asset from covenant - * Output[0]: Settlement asset → user (any address) - */ -fn withdraw_path(sig: Signature) { - assert!(jet::eq_32(jet::current_index(), 0)); - - let (input_asset, input_amount): (u256, u64) = get_input_explicit_asset_amount(0); - assert!(jet::eq_256(input_asset, param::SETTLEMENT_ASSET_ID)); - - check_user_signature(sig); - - ensure_output_asset_with_amount_eq(0, param::SETTLEMENT_ASSET_ID, input_amount); -} - -/* - * Expiry Path - * - * User reclaims remaining collateral and premium after expiry. - * Only allowed after EXPIRY_TIME. - * Requires signature from USER_PUBKEY. - * No change - full reclaim only. - * - * Layout: - * Input[0]: Collateral from covenant - * Input[1]: Premium from covenant - * Output[0]: Collateral → user (any address) - * Output[1]: Premium → user (any address) - */ -fn expiry_path(sig: Signature) { - jet::check_lock_time(param::EXPIRY_TIME); - - assert!(jet::le_32(jet::current_index(), 1)); - - let expected_covenant_script_hash: u256 = get_input_script_hash(0); - - assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); - - let (collateral_asset, collateral_amount): (u256, u64) = get_input_explicit_asset_amount(0); - assert!(jet::eq_256(collateral_asset, param::COLLATERAL_ASSET_ID)); - - let (premium_asset, premium_amount): (u256, u64) = get_input_explicit_asset_amount(1); - assert!(jet::eq_256(premium_asset, param::PREMIUM_ASSET_ID)); - - check_user_signature(sig); - - ensure_output_asset_with_amount_eq(0, param::COLLATERAL_ASSET_ID, collateral_amount); - ensure_output_asset_with_amount_eq(1, param::PREMIUM_ASSET_ID, premium_amount); -} - -fn main() { - match witness::PATH { - Left(params: (u64, bool)) => { - let (collateral_amount, is_change_needed): (u64, bool) = params; - exercise_path(collateral_amount, is_change_needed) - }, - Right(withdraw_or_expiry: Either) => match withdraw_or_expiry { - Left(sig: Signature) => withdraw_path(sig), - Right(sig: Signature) => expiry_path(sig), - }, - } -} diff --git a/crates/simplex/examples/source_simf/simple_storage.simf b/crates/simplex/examples/source_simf/simple_storage.simf deleted file mode 100644 index 7ae6c41..0000000 --- a/crates/simplex/examples/source_simf/simple_storage.simf +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Simple Storage Program for Liquid - * - * Only the owner of the storage can modify the value. - * - * ==== IMPORTANT ==== - * - * Based on the following resources: - * https://github.com/ElementsProject/elements/blob/master/src/consensus/amount.h - * https://github.com/ElementsProject/rust-elements/blob/f6ffc7800df14b81c0f5ae1c94368a78b99612b9/src/blind.rs#L471 - * - * The maximum allowed amount is 2,100,000,000,000,000 - * (i.e., 21,000,000 × 10^8), which is approximately 51 bits. - */ - -fn checksig(pk: Pubkey, sig: Signature) { - let msg: u256 = jet::sig_all_hash(); - jet::bip_0340_verify((pk, msg), sig); -} - -fn ensure_current_index_eq(expected_index: u32){ - assert!(jet::eq_32(jet::current_index(), expected_index)); -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn ensure_output_is_op_return(index: u32) { - match jet::output_null_datum(index, 0) { - Some(entry: Option>>) => (), - None => panic!(), - } -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } -fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } - -fn increment_by(index: u32, amount: u32) -> u32 { - let (carry, result): (bool, u32) = jet::add_32(index, amount); - ensure_zero_bit(carry); - result -} - -fn enforce_stage_checks(index: u32, new_value: u64) { - ensure_input_and_output_script_hash_eq(index); - - let (asset_bits, old_value): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(asset_bits, param::SLOT_ID)); - - ensure_output_asset_with_amount_eq(index, param::SLOT_ID, new_value); - - match jet::lt_64(new_value, old_value) { - // burn - true => { - let burn_output_index: u32 = increment_by(index, 1); - - let (carry, amount_to_burn): (bool, u64) = jet::subtract_64(old_value, new_value); - ensure_zero_bit(carry); - - ensure_output_is_op_return(burn_output_index); - ensure_output_asset_with_amount_eq(burn_output_index, param::SLOT_ID, amount_to_burn); - }, - // mint - false => { - let reissuance_output_index: u32 = increment_by(index, 1); - ensure_input_and_output_script_hash_eq(reissuance_output_index); - }, - }; -} - -fn main() { - let index: u32 = 0; - enforce_stage_checks(index, witness::NEW_VALUE); - - checksig(param::USER, witness::USER_SIGNATURE) -} diff --git a/crates/simplex/tests/compiletest.rs b/crates/simplex/tests/compiletest.rs deleted file mode 100644 index db7b77e..0000000 --- a/crates/simplex/tests/compiletest.rs +++ /dev/null @@ -1,7 +0,0 @@ -// TODO: all files has to pass - -#[test] -fn ui() { - let t = trybuild::TestCases::new(); - t.pass("tests/ui/*.rs"); -} diff --git a/crates/simplex/tests/simplex_test.rs b/crates/simplex/tests/simplex_test.rs index b4a3b4c..473de22 100644 --- a/crates/simplex/tests/simplex_test.rs +++ b/crates/simplex/tests/simplex_test.rs @@ -1,10 +1,11 @@ use simplex_provider::elements_rpc::{AddressType, ElementsRpcClient}; -use simplex_sdk::constants::SimplicityNetwork; use simplex_test::{DEFAULT_SAT_AMOUNT_FAUCET, ElementsDConf, TestContext}; use simplicityhl::elements::Address; use simplicityhl::elements::bitcoin::secp256k1; use simplicityhl::elements::secp256k1_zkp::Keypair; +use simplex::simplex_sdk::constants::SimplicityNetwork; + #[ignore] #[simplex::simplex_macros::test(default_rpc)] fn test_execution(x: TestContext) { diff --git a/crates/simplex/tests/ui/array_tr_storage.rs b/crates/simplex/tests/ui/array_tr_storage.rs index f04d583..006ed37 100644 --- a/crates/simplex/tests/ui/array_tr_storage.rs +++ b/crates/simplex/tests/ui/array_tr_storage.rs @@ -1,6 +1,5 @@ -use simplex_macros::*; -use simplex_sdk::witness::WitnessTrait; -use simplex_sdk::arguments::ArgumentsTrait; +use simplex::simplex_macros::*; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; include_simf!("../../../../crates/simplex/tests/ui/array_tr_storage.simf"); @@ -21,4 +20,4 @@ fn main() -> Result<(), String> { assert_eq!(original_arguments, recovered_witness); Ok(()) -} \ No newline at end of file +} diff --git a/crates/simplex/tests/ui/array_tr_storage.simf b/crates/simplex/tests/ui/array_tr_storage.simf deleted file mode 100644 index 4918cf3..0000000 --- a/crates/simplex/tests/ui/array_tr_storage.simf +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Extends `bytes32_tr_storage` using `array_fold` for larger buffers. - * Optimized for small, fixed-size states where linear hashing is more efficient - * than Merkle Trees. By avoiding proof overhead like sibling hashes, we reduce - * witness size and simplify contract logic for small N. - * This approach is particularly advantageous when updating all slots within every transaction. - */ - -fn hash_array_tr_storage(elem: u256, ctx: Ctx8) -> Ctx8 { - jet::sha_256_ctx_8_add_32(ctx, elem) -} - -fn hash_array_tr_storage_with_update(elem: u256, triplet: (Ctx8, u16, u16)) -> (Ctx8, u16, u16) { - let (ctx, i, changed_index): (Ctx8, u16, u16) = triplet; - - match jet::eq_16(i, changed_index) { - true => { - let (_, val): (bool, u16) = jet::increment_16(i); - - // There may be arbitrary logic here - let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(elem); - let new_state4: u64 = 20; - - let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); - ( - jet::sha_256_ctx_8_add_32(ctx, new_state), - val, - changed_index, - ) - }, - false => { - let (_, val): (bool, u16) = jet::increment_16(i); - ( - jet::sha_256_ctx_8_add_32(ctx, elem), - val, - changed_index, - ) - } - } -} - -fn script_hash_for_input_script(state: [u256; 3], changed_index: Option) -> u256 { - let tap_leaf: u256 = jet::tapleaf_hash(); - let ctx: Ctx8 = jet::tapdata_init(); - - let (ctx, _, _): (Ctx8, u16, u16) = match changed_index { - Some(ind: u16) => { - array_fold::(state, (ctx, 0, ind)) - }, - None => { - (array_fold::(state, ctx), 0, 0) - } - }; - - let computed: u256 = jet::sha_256_ctx_8_finalize(ctx); - let tap_node: u256 = jet::build_tapbranch(tap_leaf, computed); - - let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; - let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); - - let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); - let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 - let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); - jet::sha_256_ctx_8_finalize(hash_ctx3) -} - -fn main() { - let state: [u256; 3] = witness::STATE; - - // Assert that the input is correct, i.e. "load". - assert!(jet::eq_256( - script_hash_for_input_script(state, None), - unwrap(jet::input_script_hash(jet::current_index())) - )); - - // Assert that the output is correct, i.e. "store". - assert!(jet::eq_256( - script_hash_for_input_script(state, Some(witness::CHANGED_INDEX)), - unwrap(jet::output_script_hash(jet::current_index())) - )); -} \ No newline at end of file diff --git a/crates/simplex/tests/ui/bytes32_tr_storage.rs b/crates/simplex/tests/ui/bytes32_tr_storage.rs index 5937bcc..241431d 100644 --- a/crates/simplex/tests/ui/bytes32_tr_storage.rs +++ b/crates/simplex/tests/ui/bytes32_tr_storage.rs @@ -1,6 +1,5 @@ -use simplex_macros::*; -use simplex_sdk::witness::WitnessTrait; -use simplex_sdk::arguments::ArgumentsTrait; +use simplex::simplex_macros::*; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; include_simf!("../../../../crates/simplex/tests/ui/bytes32_tr_storage.simf"); @@ -20,4 +19,4 @@ fn main() -> Result<(), String> { assert_eq!(original_arguments, recovered_witness); Ok(()) -} \ No newline at end of file +} diff --git a/crates/simplex/tests/ui/bytes32_tr_storage.simf b/crates/simplex/tests/ui/bytes32_tr_storage.simf deleted file mode 100644 index 0d11b5f..0000000 --- a/crates/simplex/tests/ui/bytes32_tr_storage.simf +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Computes the "State Commitment" — the expected Script PubKey (address) - * for a specific state value. - * - * HOW IT WORKS: - * In Simplicity/Liquid, state is not stored in a dedicated database. Instead, - * it is verified via a "Commitment Scheme" inside the Taproot tree of the UTXO. - * - * This function reconstructs the Taproot structure to validate that the provided - * witness data (state_data) was indeed cryptographically embedded into the - * transaction output that is currently being spent. - * - * LOGIC FLOW: - * 1. Takes state_data (passed via witness at runtime). - * 2. Hashes it as a non-executable TapData leaf. - * 3. Combines it with the current program's CMR (tapleaf_hash). - * 4. Derives the tweaked_key (Internal Key + Merkle Root). - * 5. Returns the final SHA256 script hash (SegWit v1). - * - * USAGE: - * - In main, we verify: CalculatedHash(witness::STATE) == input_script_hash. - * - This assertion proves that the UTXO is "locked" not just by the code, - * but specifically by THIS instance of the state data. - */ - -fn script_hash_for_input_script(state_data: u256) -> u256 { - // This is the bulk of our "compute state commitment" logic from above. - let tap_leaf: u256 = jet::tapleaf_hash(); - let state_ctx1: Ctx8 = jet::tapdata_init(); - let state_ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(state_ctx1, state_data); - let state_leaf: u256 = jet::sha_256_ctx_8_finalize(state_ctx2); - let tap_node: u256 = jet::build_tapbranch(tap_leaf, state_leaf); - - // Compute a taptweak using this. - let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; - let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); - - // Turn the taptweak into a script hash - let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); - let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 - let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); - jet::sha_256_ctx_8_finalize(hash_ctx3) -} - -fn main() { - let state_data: u256 = witness::STATE; - let (state1, state2, state3, state4): (u64, u64, u64, u64) = ::into(state_data); - - // Assert that the input is correct, i.e. "load". - assert!(jet::eq_256( - script_hash_for_input_script(state_data), - unwrap(jet::input_script_hash(jet::current_index())) - )); - - // Do a state update (and fail on 64-bit overflow even though we've got 192 other - // bits we could be using..) - let (carry, new_state4): (bool, u64) = jet::increment_64(state4); - assert!(jet::eq_1(::into(carry), 0)); - - let new_state: u256 = <(u64, u64, u64, u64)>::into((state1, state2, state3, new_state4)); - // Assert that the output is correct, i.e. "store". - assert!(jet::eq_256( - script_hash_for_input_script(new_state), - unwrap(jet::output_script_hash(jet::current_index())) - )); -} \ No newline at end of file diff --git a/crates/simplex/tests/ui/dual_currency_deposit.rs b/crates/simplex/tests/ui/dual_currency_deposit.rs index 466fd77..9919676 100644 --- a/crates/simplex/tests/ui/dual_currency_deposit.rs +++ b/crates/simplex/tests/ui/dual_currency_deposit.rs @@ -1,6 +1,5 @@ -use simplex_macros::*; -use simplex_sdk::witness::WitnessTrait; -use simplex_sdk::arguments::ArgumentsTrait; +use simplex::simplex_macros::*; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; include_simf!("../../../../crates/simplex/tests/ui/dual_currency_deposit.simf"); @@ -46,4 +45,4 @@ fn main() -> Result<(), String> { assert_eq!(original_arguments, recovered_witness); Ok(()) -} \ No newline at end of file +} diff --git a/crates/simplex/tests/ui/dual_currency_deposit.simf b/crates/simplex/tests/ui/dual_currency_deposit.simf deleted file mode 100644 index e1a460a..0000000 --- a/crates/simplex/tests/ui/dual_currency_deposit.simf +++ /dev/null @@ -1,592 +0,0 @@ -/* - * DCD: Dual Currency Deposit – price-attested settlement and funding windows - * - * Flows implemented: - * - Maker funding: deposit settlement asset and collateral, issue grantor tokens - * - Taker funding: deposit collateral in window and receive filler tokens - * - Settlement: at SETTLEMENT_HEIGHT, oracle Schnorr signature over (height, price) - * selects LBTC vs ALT branch based on price <= STRIKE_PRICE - * - Early/post-expiry termination: taker returns filler; maker burns grantor tokens - * - Merge: consolidate 2/3/4 token UTXOs - * - * All amounts and asset/script invariants are enforced on-chain; time guards use - * fallback locktime and height checks. - * - * Batching discussion: https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 - */ - -// Verify Schnorr signature against SHA256 of (u32 || u64) -fn checksig_priceblock(pk: Pubkey, current_block_height: u32, price_at_current_block_height: u64, sig: Signature) { - let hasher: Ctx8 = jet::sha_256_ctx_8_init(); - let hasher: Ctx8 = jet::sha_256_ctx_8_add_4(hasher, current_block_height); - let hasher: Ctx8 = jet::sha_256_ctx_8_add_8(hasher, price_at_current_block_height); - let msg: u256 = jet::sha_256_ctx_8_finalize(hasher); - jet::bip_0340_verify((pk, msg), sig); -} - -// Signed <= using XOR with 0x8000.. bias: a<=b (signed) iff (a^bias) <= (b^bias) (unsigned) -fn signed_le_u64(a_bits: u64, b_bits: u64) -> bool { - let bias: u64 = 0x8000000000000000; - jet::le_64(jet::xor_64(a_bits, bias), jet::xor_64(b_bits, bias)) -} - -fn signed_lt_u64(a: u64, b: u64) -> bool { - let bias: u64 = 0x8000000000000000; - jet::lt_64(jet::xor_64(a, bias), jet::xor_64(b, bias)) -} - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -/// Assert: base_amount * basis_point_percentage == provided_amount * MAX_BASIS_POINTS -fn constraint_percentage(base_amount: u64, basis_point_percentage: u64, provided_amount: u64) { - let MAX_BASIS_POINTS: u64 = 10000; - - let arg1: u256 = <(u128, u128)>::into((0, jet::multiply_64(base_amount, basis_point_percentage))); - let arg2: u256 = <(u128, u128)>::into((0, jet::multiply_64(provided_amount, MAX_BASIS_POINTS))); - - assert!(jet::eq_256(arg1, arg2)); -} - -fn get_output_script_hash(index: u32) -> u256 { - unwrap(jet::output_script_hash(index)) -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } -fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } - -fn ensure_one_bit_or(bit1: bool, bit2: bool) { - assert!( - jet::eq_1( - ::into(jet::or_1(::into(bit1), ::into(bit2))), - 1 - ) - ); -} - -fn increment_by(index: u32, amount: u32) -> u32 { - let (carry, result): (bool, u32) = jet::add_32(index, amount); - ensure_zero_bit(carry); - result -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn ensure_output_is_op_return(index: u32) { - match jet::output_null_datum(index, 0) { - Some(entry: Option>>) => (), - None => panic!(), - } -} - -fn ensure_input_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::input_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::output_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_input_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - assert!(jet::eq_32(jet::current_index(), index)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -fn merge_2_tokens() { - // 2 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 3)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 1)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); -} - -fn merge_3_tokens() { - // 3 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 4)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 2)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); - assert!(jet::eq_256(script_hash, get_input_script_hash(2))); -} - -fn merge_4_tokens() { - // 4 tokens to merge + 1 input as fee - assert!(jet::eq_32(jet::num_inputs(), 5)); - // 3 outputs: 1 merged token + 1 change + 1 fee - assert!(jet::eq_32(jet::num_outputs(), 3)); - assert!(jet::le_32(jet::current_index(), 3)); - - ensure_input_and_output_script_hash_eq(0); - let script_hash: u256 = get_input_script_hash(0); - assert!(jet::eq_256(script_hash, get_input_script_hash(1))); - assert!(jet::eq_256(script_hash, get_input_script_hash(2))); - assert!(jet::eq_256(script_hash, get_input_script_hash(3))); -} - -/* -* Maker funding path -* Params: -* 1. FILLER_PER_SETTLEMENT_COLLATERAL -* 2. FILLER_PER_SETTLEMENT_ASSET -* 3. FILLER_PER_PRINCIPAL_COLLATERAL -* 4. GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET -* 5. GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL -* 6. GRANTOR_PER_SETTLEMENT_COLLATERAL -* 7. GRANTOR_PER_SETTLEMENT_ASSET -*/ -fn maker_funding_path(principal_collateral_amount: u64, principal_asset_amount: u64, interest_collateral_amount: u64, interest_asset_amount: u64) { - assert!(jet::eq_32(jet::num_inputs(), 5)); - assert!(jet::eq_32(jet::num_outputs(), 11)); - - let current_time: u32 = ::into(jet::lock_time()); - assert!(jet::lt_32(current_time, param::TAKER_FUNDING_START_TIME)); - - ensure_input_and_output_script_hash_eq(0); - ensure_input_and_output_script_hash_eq(1); - ensure_input_and_output_script_hash_eq(2); - - assert!(jet::le_32(jet::current_index(), 2)); - - let script_hash: u256 = get_output_script_hash(0); - ensure_output_script_hash_eq(1, script_hash); - ensure_output_script_hash_eq(2, script_hash); - ensure_output_script_hash_eq(3, script_hash); - ensure_output_script_hash_eq(4, script_hash); - ensure_output_script_hash_eq(5, script_hash); - - let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(3); - let (settlement_asset_bits, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(4); - let filler_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); - let grantor_collateral_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); - let grantor_settlement_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(2)))); - assert!(jet::eq_64(filler_token_amount, grantor_collateral_token_amount)); - assert!(jet::eq_64(filler_token_amount, grantor_settlement_token_amount)); - - divmod_eq(principal_asset_amount, param::STRIKE_PRICE, principal_collateral_amount); - - assert!(jet::eq_64(collateral_amount, interest_collateral_amount)); - constraint_percentage(principal_collateral_amount, param::INCENTIVE_BASIS_POINTS, collateral_amount); - - let MAX_BASIS_POINTS: u64 = 10000; - let (carry, asset_incentive_percentage): (bool, u64) = jet::add_64(param::INCENTIVE_BASIS_POINTS, MAX_BASIS_POINTS); - ensure_zero_bit(carry); - - constraint_percentage(principal_asset_amount, asset_incentive_percentage, settlement_amount); - - let (carry, calculated_total_asset_amount): (bool, u64) = jet::add_64(principal_asset_amount, interest_asset_amount); - ensure_zero_bit(carry); - assert!(jet::eq_64(calculated_total_asset_amount, settlement_amount)); - - let (carry, calculated_total_collateral_amount): (bool, u64) = jet::add_64(principal_collateral_amount, interest_collateral_amount); - ensure_zero_bit(carry); - - // Filler token constraints - divmod_eq(calculated_total_collateral_amount, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_token_amount); - divmod_eq(calculated_total_asset_amount, param::FILLER_PER_SETTLEMENT_ASSET, filler_token_amount); - divmod_eq(principal_collateral_amount, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount); - - // Grantor token constraints - divmod_eq(calculated_total_asset_amount, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_token_amount); - divmod_eq(interest_collateral_amount, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_token_amount); - - divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_collateral_token_amount); - // divmod_eq(calculated_total_collateral_amount, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_settlement_token_amount); // duplicated because of lines 203-204 - - divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_collateral_token_amount); - // divmod_eq(calculated_total_asset_amount, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_settlement_token_amount); // duplicated because of lines 203-204 - - assert!(jet::eq_256(param::COLLATERAL_ASSET_ID, collateral_asset_bits)); - assert!(jet::eq_256(param::SETTLEMENT_ASSET_ID, settlement_asset_bits)); - - ensure_output_asset_with_amount_eq(5, param::FILLER_TOKEN_ASSET, filler_token_amount); - ensure_output_asset_with_amount_eq(6, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_token_amount); - ensure_output_asset_with_amount_eq(7, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_token_amount); - - ensure_input_asset_eq(3, param::SETTLEMENT_ASSET_ID); - ensure_input_asset_eq(4, param::COLLATERAL_ASSET_ID); - - ensure_output_asset_eq(8, param::COLLATERAL_ASSET_ID); - ensure_output_asset_eq(9, param::SETTLEMENT_ASSET_ID); - ensure_output_asset_eq(10, param::COLLATERAL_ASSET_ID); -} - -fn taker_funding_path(collateral_amount_to_deposit: u64, filler_token_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - assert!(jet::le_32(param::TAKER_FUNDING_START_TIME, current_time)); - assert!(jet::lt_32(current_time, param::TAKER_FUNDING_END_TIME)); - assert!(jet::lt_32(current_time, param::CONTRACT_EXPIRY_TIME)); - - let filler_token_input_index: u32 = 0; - let collateral_input_index: u32 = 1; - - let (collateral_to_covenant_output_index, filler_to_user_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(filler_token_input_index); - - // Check and ensure filler token change - ensure_correct_change_at_index(0, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_deposit, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_get); - - // Ensure collateral asset and script hash are correct - ensure_output_asset_with_amount_eq(collateral_to_covenant_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_deposit); - ensure_output_script_hash_eq(collateral_to_covenant_output_index, expected_current_script_hash); - - ensure_output_asset_with_amount_eq(filler_to_user_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_get); -} - -fn taker_early_termination_path(filler_token_amount_to_return: u64, collateral_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let collateral_input_index: u32 = 0; - let filler_token_input_index: u32 = 1; - - let (return_filler_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::FILLER_PER_PRINCIPAL_COLLATERAL, filler_token_amount_to_return); - - // Ensure filler token transferred to covenant - ensure_output_asset_with_amount_eq(return_filler_output_index, param::FILLER_TOKEN_ASSET, filler_token_amount_to_return); - ensure_output_script_hash_eq(return_filler_output_index, expected_current_script_hash); - - // Ensure collateral transferred to user - ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); -} - -fn maker_collateral_termination_path(grantor_collateral_amount_to_burn: u64, collateral_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let collateral_input_index: u32 = 0; - let grantor_collateral_token_input_index: u32 = 1; - - let (burn_grantor_collateral_output_index, return_collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::GRANTOR_COLLATERAL_PER_DEPOSITED_COLLATERAL, grantor_collateral_amount_to_burn); - - // Burn grantor collateral token - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_collateral_amount_to_burn); - - // Ensure collateral transferred to user - ensure_output_asset_with_amount_eq(return_collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount_to_get); -} - -fn maker_settlement_termination_path(grantor_settlement_amount_to_burn: u64, settlement_amount_to_get: u64, is_change_needed: bool) { - let current_time: u32 = ::into(jet::lock_time()); - ensure_one_bit_or(jet::le_32(current_time, param::EARLY_TERMINATION_END_TIME), jet::le_32(param::CONTRACT_EXPIRY_TIME, current_time)); - - let settlement_asset_input_index: u32 = 0; - let grantor_settlement_token_input_index: u32 = 1; - - let (burn_grantor_settlement_output_index, return_settlement_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(settlement_amount_to_get, param::GRANTOR_SETTLEMENT_PER_DEPOSITED_ASSET, grantor_settlement_amount_to_burn); - - // Burn grantor settlement token - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_settlement_amount_to_burn); - - // Ensure settlement asset transferred to user - ensure_output_asset_with_amount_eq(return_settlement_output_index, param::SETTLEMENT_ASSET_ID, settlement_amount_to_get); -} - -fn ensure_correct_return_at(user_output_index: u32, asset_id: u256, amount_to_get: u64, fee_basis_points: u64) { - match jet::eq_64(fee_basis_points, 0) { - true => ensure_output_asset_with_amount_eq(user_output_index, asset_id, amount_to_get), - false => { - let fee_output_index: u32 = increment_by(user_output_index, 1); - - let (user_asset_bits, user_amount): (u256, u64) = get_output_explicit_asset_amount(user_output_index); - assert!(jet::eq_256(user_asset_bits, asset_id)); - - let (fee_asset_bits, fee_amount): (u256, u64) = get_output_explicit_asset_amount(fee_output_index); - assert!(jet::eq_256(fee_asset_bits, asset_id)); - - let (carry, calculated_total_amount): (bool, u64) = jet::add_64(user_amount, fee_amount); - ensure_zero_bit(carry); - - constraint_percentage(calculated_total_amount, fee_basis_points, fee_amount); - - ensure_output_script_hash_eq(fee_output_index, param::FEE_SCRIPT_HASH); - }, - }; -} - -fn maker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, grantor_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { - jet::check_lock_height(param::SETTLEMENT_HEIGHT); - checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); - - match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { - true => { - // Maker gets ALT - let settlement_asset_input_index: u32 = 0; - - let (burn_grantor_settlement_output_index, burn_grantor_collateral_output_index, settlement_output_index): (u32, u32, u32) = match is_change_needed { - true => (1, 2, 3), - false => (0, 1, 2), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_ASSET, grantor_amount_to_burn); - - // Burn grantor settlement and collateral tokens - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); - - // Ensure settlement asset transferred to user - ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - false => { - // Maker gets the LBTC - let collateral_input_index: u32 = 0; - - let (burn_grantor_collateral_output_index, burn_grantor_settlement_output_index, collateral_output_index): (u32, u32, u32) = match is_change_needed { - true => (1, 2, 3), - false => (0, 1, 2), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(amount_to_get, param::GRANTOR_PER_SETTLEMENT_COLLATERAL, grantor_amount_to_burn); - - // Burn grantor collateral and settlement tokens - ensure_output_is_op_return(burn_grantor_collateral_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_collateral_output_index, param::GRANTOR_COLLATERAL_TOKEN_ASSET, grantor_amount_to_burn); - ensure_output_is_op_return(burn_grantor_settlement_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_settlement_output_index, param::GRANTOR_SETTLEMENT_TOKEN_ASSET, grantor_amount_to_burn); - - // Ensure collateral transferred to user - ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - } -} - -fn taker_settlement_path(price_at_current_block_height: u64, oracle_sig: Signature, filler_amount_to_burn: u64, amount_to_get: u64, is_change_needed: bool) { - jet::check_lock_height(param::SETTLEMENT_HEIGHT); - checksig_priceblock(param::ORACLE_PK, param::SETTLEMENT_HEIGHT, price_at_current_block_height, oracle_sig); - - match jet::le_64(price_at_current_block_height, param::STRIKE_PRICE) { - true => { - // Taker receives LBTC principal+interest - let collateral_input_index: u32 = 0; - - let (burn_filler_output_index, collateral_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_COLLATERAL, filler_amount_to_burn); - - // Burn filler token - ensure_output_is_op_return(burn_filler_output_index); - ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); - - // Ensure collateral transferred to user - ensure_correct_return_at(collateral_output_index, param::COLLATERAL_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - false => { - // Taker receives ALT - let settlement_asset_input_index: u32 = 0; - - let (burn_filler_output_index, settlement_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(settlement_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset amount is correct - divmod_eq(amount_to_get, param::FILLER_PER_SETTLEMENT_ASSET, filler_amount_to_burn); - - // Burn filler token - ensure_output_is_op_return(burn_filler_output_index); - ensure_output_asset_with_amount_eq(burn_filler_output_index, param::FILLER_TOKEN_ASSET, filler_amount_to_burn); - - // Ensure filler token transferred to user - ensure_correct_return_at(settlement_output_index, param::SETTLEMENT_ASSET_ID, amount_to_get, param::FEE_BASIS_POINTS); - }, - } -} - -fn main() { - let token_branch: Either<(), ()> = witness::TOKEN_BRANCH; - let merge_branch: Either, ()> = witness::MERGE_BRANCH; - - match witness::PATH { - Left(funding_or_settlement: Either, (u64, Signature, u64, u64, bool)>) => match funding_or_settlement { - // Funding branches - Left(funding_params: Either<(u64, u64, u64, u64), (u64, u64, bool)>) => match funding_params { - // Maker funding: (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) - Left(params: (u64, u64, u64, u64)) => { - let (principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount): (u64, u64, u64, u64) = params; - maker_funding_path(principal_collateral_amount, principal_asset_amount, interest_collateral_amount, interest_asset_amount) - }, - // Taker funding: (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) - Right(params: (u64, u64, bool)) => { - let (collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed): (u64, u64, bool) = params; - taker_funding_path(collateral_amount_to_deposit, filler_token_amount_to_get, is_change_needed) - }, - }, - // Settlement branches (oracle price attested) - Right(params: (u64, Signature, u64, u64, bool)) => { - let (price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed): (u64, Signature, u64, u64, bool) = params; - - match token_branch { - // Maker settlement: burn grantor token - Left(u: ()) => maker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), - // Taker settlement: burn filler token - Right(u: ()) => taker_settlement_path(price_at_current_block_height, oracle_sig, amount_to_burn, amount_to_get, is_change_needed), - } - }, - }, - // Termination flows (early termination or post-expiry) or Merge flows - Right(termination_or_maker_or_merge: Either, ()>) => match termination_or_maker_or_merge { - Left(termination_or_maker: Either<(bool, u64, u64), (bool, u64, u64)>) => match termination_or_maker { - // Taker early termination: (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get) - Left(params: (bool, u64, u64)) => { - let (is_change_needed, filler_token_amount_to_return, collateral_amount_to_get): (bool, u64, u64) = params; - taker_early_termination_path(filler_token_amount_to_return, collateral_amount_to_get, is_change_needed) - }, - // Maker termination (burn grantor token): choose collateral vs settlement token via token_branch - Right(params: (bool, u64, u64)) => { - let (is_change_needed, grantor_token_amount_to_burn, amount_to_get): (bool, u64, u64) = params; - - match token_branch { - // Burn grantor collateral token -> receive collateral - Left(u: ()) => maker_collateral_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), - // Burn grantor settlement token -> receive settlement asset - Right(u: ()) => maker_settlement_termination_path(grantor_token_amount_to_burn, amount_to_get, is_change_needed), - } - }, - }, - Right(u: ()) => { - // Merge tokens based on MERGE_BRANCH discriminator - match merge_branch { - Left(left_or_right: Either<(), ()>) => match left_or_right { - Left(u: ()) => merge_2_tokens(), - Right(u: ()) => merge_3_tokens(), - }, - Right(u: ()) => merge_4_tokens(), - } - }, - }, - } - -} diff --git a/crates/simplex/tests/ui/dual_currency_deposit.stderr b/crates/simplex/tests/ui/dual_currency_deposit.stderr deleted file mode 100644 index 6b486b3..0000000 --- a/crates/simplex/tests/ui/dual_currency_deposit.stderr +++ /dev/null @@ -1,21 +0,0 @@ -warning: unreachable expression - --> tests/ui/dual_currency_deposit.rs:3:1 - | -3 | include_simf!("../../../../crates/core/tests/ui/dual_currency_deposit.simf"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | | - | unreachable expression - | any code following this expression is unreachable - | - = note: `#[warn(unreachable_code)]` (part of `#[warn(unused)]`) on by default - = note: this warning originates in the macro `include_simf` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0308]: mismatched types - --> tests/ui/dual_currency_deposit.rs:3:1 - | -3 | include_simf!("../../../../crates/core/tests/ui/dual_currency_deposit.simf"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `[u8; 64]`, found `!` - | - = note: expected enum `simplicityhl::either::Either, (u64, [u8; 64], u64, u64, bool)>, simplicityhl::either::Either, ()>>` - found enum `simplicityhl::either::Either, (u64, !, u64, u64, bool)>, simplicityhl::either::Either, ()>>` - = note: this error originates in the macro `include_simf` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/simplex/tests/ui/option_offer.rs b/crates/simplex/tests/ui/option_offer.rs index 96d7eaa..1cfc681 100644 --- a/crates/simplex/tests/ui/option_offer.rs +++ b/crates/simplex/tests/ui/option_offer.rs @@ -1,6 +1,5 @@ -use simplex_macros::*; -use simplex_sdk::witness::WitnessTrait; -use simplex_sdk::arguments::ArgumentsTrait; +use simplex::simplex_macros::*; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; include_simf!("../../../../crates/simplex/tests/ui/option_offer.simf"); @@ -26,4 +25,4 @@ fn main() -> Result<(), String> { assert_eq!(original_arguments, recovered_witness); Ok(()) -} \ No newline at end of file +} diff --git a/crates/simplex/tests/ui/option_offer.simf b/crates/simplex/tests/ui/option_offer.simf deleted file mode 100644 index 5cb2108..0000000 --- a/crates/simplex/tests/ui/option_offer.simf +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Option Offer - * - * A covenant that allows a user to deposit collateral and premium assets, - * and have a counterparty swap settlement asset for both. - * The user can withdraw accumulated settlement asset at any time (with signature). - * After expiry, the user can reclaim any remaining collateral and premium (with signature). - * - * Paths: - * 1. Exercise: Counterparty swaps settlement asset for collateral + premium (no time restriction, optional change) - * 2. Withdraw: User withdraws settlement asset (no time restriction, signature required, full amount) - * 3. Expiry: User reclaims collateral + premium (after expiry, signature required, full amount) - * - * Constraints: - * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount - * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount - */ - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_zero_bit(bit: bool) { - assert!(jet::eq_1(::into(bit), 0)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = get_output_explicit_asset_amount(index); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn check_user_signature(sig: Signature) { - let msg: u256 = jet::sig_all_hash(); - jet::bip_0340_verify((param::USER_PUBKEY, msg), sig); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, asset_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, asset_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -/* - * Exercise Path - * - * Counterparty swaps settlement asset for collateral + premium. - * No time restriction - works before and after expiry. - * - * Constraints: - * settlement_amount = COLLATERAL_PER_CONTRACT * collateral_amount - * premium_amount = PREMIUM_PER_COLLATERAL * collateral_amount - * - * Layout: - * - * Both: - * Input[0]: Collateral from covenant - * Input[1]: Premium from covenant - * - * With change (partial swap): - * Output[0]: Collateral change → covenant - * Output[1]: Premium change → covenant - * Output[2]: Settlement asset → covenant - * Output[3]: Collateral → counterparty - * Output[4]: Premium → counterparty - * - * Without change (full swap): - * Output[0]: Settlement asset → covenant - * Output[1]: Collateral → counterparty - * Output[2]: Premium → counterparty - */ -fn exercise_path(collateral_amount: u64, is_change_needed: bool) { - assert!(jet::le_32(jet::current_index(), 1)); - - let expected_covenant_script_hash: u256 = get_input_script_hash(0); - - assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); - - let premium_amount_u128: u128 = jet::multiply_64(collateral_amount, param::PREMIUM_PER_COLLATERAL); - let (left_part, premium_amount): (u64, u64) = dbg!(::into(premium_amount_u128)); - assert!(jet::eq_64(left_part, 0)); - - // Check collateral changes - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_covenant_script_hash, is_change_needed); - ensure_correct_change_at_index(1, param::PREMIUM_ASSET_ID, premium_amount, expected_covenant_script_hash, is_change_needed); - - let (settlement_output_index, collateral_output_index, premium_output_index): (u32, u32, u32) = match is_change_needed { - true => (2, 3, 4), - false => (0, 1, 2), - }; - - ensure_output_script_hash_eq(settlement_output_index, expected_covenant_script_hash); - - let (output_asset, settlement_amount): (u256, u64) = get_output_explicit_asset_amount(settlement_output_index); - assert!(jet::eq_256(output_asset, param::SETTLEMENT_ASSET_ID)); - - divmod_eq(settlement_amount, param::COLLATERAL_PER_CONTRACT, collateral_amount); - - ensure_output_asset_with_amount_eq(collateral_output_index, param::COLLATERAL_ASSET_ID, collateral_amount); - ensure_output_asset_with_amount_eq(premium_output_index, param::PREMIUM_ASSET_ID, premium_amount); -} - -/* - * Withdraw Path - * - * User withdraws accumulated settlement asset. - * No time restriction. - * Requires signature from USER_PUBKEY. - * No change - full withdrawal only. - * - * Layout: - * Input[0]: Settlement asset from covenant - * Output[0]: Settlement asset → user (any address) - */ -fn withdraw_path(sig: Signature) { - assert!(jet::eq_32(jet::current_index(), 0)); - - let (input_asset, input_amount): (u256, u64) = get_input_explicit_asset_amount(0); - assert!(jet::eq_256(input_asset, param::SETTLEMENT_ASSET_ID)); - - check_user_signature(sig); - - ensure_output_asset_with_amount_eq(0, param::SETTLEMENT_ASSET_ID, input_amount); -} - -/* - * Expiry Path - * - * User reclaims remaining collateral and premium after expiry. - * Only allowed after EXPIRY_TIME. - * Requires signature from USER_PUBKEY. - * No change - full reclaim only. - * - * Layout: - * Input[0]: Collateral from covenant - * Input[1]: Premium from covenant - * Output[0]: Collateral → user (any address) - * Output[1]: Premium → user (any address) - */ -fn expiry_path(sig: Signature) { - jet::check_lock_time(param::EXPIRY_TIME); - - assert!(jet::le_32(jet::current_index(), 1)); - - let expected_covenant_script_hash: u256 = get_input_script_hash(0); - - assert!(jet::eq_256(get_input_script_hash(1), expected_covenant_script_hash)); - - let (collateral_asset, collateral_amount): (u256, u64) = get_input_explicit_asset_amount(0); - assert!(jet::eq_256(collateral_asset, param::COLLATERAL_ASSET_ID)); - - let (premium_asset, premium_amount): (u256, u64) = get_input_explicit_asset_amount(1); - assert!(jet::eq_256(premium_asset, param::PREMIUM_ASSET_ID)); - - check_user_signature(sig); - - ensure_output_asset_with_amount_eq(0, param::COLLATERAL_ASSET_ID, collateral_amount); - ensure_output_asset_with_amount_eq(1, param::PREMIUM_ASSET_ID, premium_amount); -} - -fn main() { - match witness::PATH { - Left(params: (u64, bool)) => { - let (collateral_amount, is_change_needed): (u64, bool) = params; - exercise_path(collateral_amount, is_change_needed) - }, - Right(withdraw_or_expiry: Either) => match withdraw_or_expiry { - Left(sig: Signature) => withdraw_path(sig), - Right(sig: Signature) => expiry_path(sig), - }, - } -} diff --git a/crates/simplex/tests/ui/options.rs b/crates/simplex/tests/ui/options.rs index fd92cf9..80d8aef 100644 --- a/crates/simplex/tests/ui/options.rs +++ b/crates/simplex/tests/ui/options.rs @@ -1,6 +1,5 @@ -use simplex_macros::*; -use simplex_sdk::witness::WitnessTrait; -use simplex_sdk::arguments::ArgumentsTrait; +use simplex::simplex_macros::*; +use simplex::simplex_sdk::program::{WitnessTrait, ArgumentsTrait}; include_simf!("../../../../crates/simplex/tests/ui/options.simf"); @@ -31,4 +30,4 @@ fn main() -> Result<(), String> { assert_eq!(original_arguments, recovered_witness); Ok(()) -} \ No newline at end of file +} diff --git a/crates/simplex/tests/ui/options.simf b/crates/simplex/tests/ui/options.simf deleted file mode 100644 index e7da014..0000000 --- a/crates/simplex/tests/ui/options.simf +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Options - * - * Important: Currently only the LBTC collateral is supported. - * - * Based on the https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf - * - * This contract implements cash-settled European-style options using covenant-locked collateral. - * - * Room for optimization: - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/2 (Use input asset to determine option covenent type) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/3 (Simplify match token_branch in funding_path.) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 (why batching is hard to implement) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/5 (Reduce Contract Parameters) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/21 (explains why funding is limited) - */ - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -fn get_output_script_hash(index: u32) -> u256 { - unwrap(jet::output_script_hash(index)) -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } -fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } - -fn increment_by(index: u32, amount: u32) -> u32 { - let (carry, result): (bool, u32) = jet::add_32(index, amount); - ensure_zero_bit(carry); - result -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn ensure_output_is_op_return(index: u32) { - match jet::output_null_datum(index, 0) { - Some(entry: Option>>) => (), - None => panic!(), - } -} - -fn ensure_input_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::input_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::output_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_input_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - assert!(jet::eq_32(jet::current_index(), index)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -fn check_y(expected_y: Fe, actual_y: Fe) { - match jet::eq_256(expected_y, actual_y) { - true => {}, - false => { - assert!(jet::eq_256(expected_y, jet::fe_negate(actual_y))); - } - }; -} - -fn ensure_input_and_output_reissuance_token_eq(index: u32) { - let (input_asset, input_amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (output_asset, output_amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); - - match (input_asset) { - Left(in_conf: Point) => { - let (input_asset_parity, input_asset_x): (u1, u256) = in_conf; - let (output_asset_parity, output_asset_x): (u1, u256) = unwrap_left::(output_asset); - - assert!(jet::eq_1(input_asset_parity, output_asset_parity)); - assert!(jet::eq_256(input_asset_x, output_asset_x)); - }, - Right(in_expl: u256) => { - let out_expl: u256 = unwrap_right::(output_asset); - assert!(jet::eq_256(in_expl, out_expl)); - } - }; - - match (input_amount) { - Left(in_conf: Point) => { - let (input_amount_parity, input_amount_x): (u1, u256) = in_conf; - let (output_amount_parity, output_amount_x): (u1, u256) = unwrap_left::(output_amount); - - assert!(jet::eq_1(input_amount_parity, output_amount_parity)); - assert!(jet::eq_256(input_amount_x, output_amount_x)); - }, - Right(in_expl: u64) => { - let out_expl: u64 = unwrap_right::(output_amount); - assert!(jet::eq_64(in_expl, out_expl)); - } - }; -} - -// Verify that a reissuance token commitment matches the expected token ID using provided blinding factors. -// Reissuance tokens are confidential because, in Elements, -// the asset must be provided in blinded form in order to reissue tokens. -// https://github.com/BlockstreamResearch/simplicity-contracts/issues/21#issuecomment-3691599583 -fn verify_token_commitment(actual_asset: Asset1, actual_amount: Amount1, expected_token_id: u256, abf: u256, vbf: u256) { - match actual_asset { - Left(conf_token: Point) => { - let amount_scalar: u256 = 1; - let (actual_ax, actual_ay): Ge = unwrap(jet::decompress(conf_token)); - - let gej_point: Gej = (jet::hash_to_curve(expected_token_id), 1); - let asset_blind_point: Gej = jet::generate(abf); - - let asset_generator: Gej = jet::gej_add(gej_point, asset_blind_point); - let (ax, ay): Ge = unwrap(jet::gej_normalize(asset_generator)); - - assert!(jet::eq_256(actual_ax, ax)); - check_y(actual_ay, ay); - - // Check amount - let conf_val: Point = unwrap_left::(actual_amount); - let (actual_vx, actual_vy): Ge = unwrap(jet::decompress(conf_val)); - - let amount_part: Gej = jet::scale(amount_scalar, asset_generator); - let vbf_part: Gej = jet::generate(vbf); - - let value_generator: Gej = jet::gej_add(amount_part, vbf_part); - let (vx, vy): Ge = unwrap(jet::gej_normalize(value_generator)); - - assert!(jet::eq_256(actual_vx, vx)); - check_y(actual_vy, vy); - }, - Right(reissuance_token: u256) => { - let expected_amount: u64 = 1; - let actual_amount: u64 = unwrap_right::(actual_amount); - - assert!(jet::eq_64(expected_amount, actual_amount)); - assert!(jet::eq_256(reissuance_token, expected_token_id)); - } - }; -} - -fn verify_output_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { - let (asset, amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); - verify_token_commitment(asset, amount, expected_token_id, abf, vbf); -} - -fn verify_input_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { - let (asset, amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); - verify_token_commitment(asset, amount, expected_token_id, abf, vbf); -} - -/* - * Funding Path - */ -fn funding_path( - expected_asset_amount: u64, - input_option_abf: u256, - input_option_vbf: u256, - input_grantor_abf: u256, - input_grantor_vbf: u256, - output_option_abf: u256, - output_option_vbf: u256, - output_grantor_abf: u256, - output_grantor_vbf: u256 -) { - ensure_input_and_output_script_hash_eq(0); - ensure_input_and_output_script_hash_eq(1); - - verify_input_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, input_option_abf, input_option_vbf); - verify_input_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, input_grantor_abf, input_grantor_vbf); - - verify_output_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, output_option_abf, output_option_vbf); - verify_output_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, output_grantor_abf, output_grantor_vbf); - - assert!(dbg!(jet::eq_256(get_output_script_hash(0), get_output_script_hash(1)))); - - assert!(jet::le_32(jet::current_index(), 1)); - - ensure_output_script_hash_eq(2, get_output_script_hash(0)); - - let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(2); - let option_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); - let grantor_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); - assert!(jet::eq_64(option_token_amount, grantor_token_amount)); - - divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, option_token_amount); - divmod_eq(expected_asset_amount, param::SETTLEMENT_PER_CONTRACT, option_token_amount); - - ensure_output_asset_with_amount_eq(2, param::COLLATERAL_ASSET_ID, collateral_amount); - ensure_output_asset_with_amount_eq(3, param::OPTION_TOKEN_ASSET, option_token_amount); - ensure_output_asset_with_amount_eq(4, param::GRANTOR_TOKEN_ASSET, grantor_token_amount); -} - -/* - * Cancellation Path - */ -fn cancellation_path(amount_to_burn: u64, collateral_amount_to_withdraw: u64, is_change_needed: bool) { - let collateral_input_index: u32 = 0; - let option_input_index: u32 = 1; - let grantor_input_index: u32 = 2; - - let (burn_option_output_index, burn_grantor_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_withdraw, expected_current_script_hash, is_change_needed); - - // Burn option and grantor tokens - ensure_output_is_op_return(burn_option_output_index); - ensure_output_is_op_return(burn_grantor_output_index); - - ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, amount_to_burn); - ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, amount_to_burn); - - // Ensure returned collateral amount is correct - divmod_eq(collateral_amount_to_withdraw, param::COLLATERAL_PER_CONTRACT, amount_to_burn); -} - -/* - * Exercise Path - */ -fn exercise_path(option_amount_to_burn: u64, collateral_amount_to_get: u64, asset_amount_to_pay: u64, is_change_needed: bool) { - jet::check_lock_time(param::START_TIME); - - let collateral_input_index: u32 = 0; - - let (burn_option_output_index, asset_to_covenant_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::COLLATERAL_PER_CONTRACT, option_amount_to_burn); - divmod_eq(asset_amount_to_pay, param::SETTLEMENT_PER_CONTRACT, option_amount_to_burn); - - // Burn option token - ensure_output_is_op_return(burn_option_output_index); - ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, option_amount_to_burn); - - // Ensure settlement asset and script hash are correct - ensure_output_asset_with_amount_eq(asset_to_covenant_output_index, param::SETTLEMENT_ASSET_ID, asset_amount_to_pay); - ensure_output_script_hash_eq(asset_to_covenant_output_index, expected_current_script_hash); -} - -/* - * Settlement Path - */ -fn settlement_path(grantor_token_amount_to_burn: u64, asset_amount: u64, is_change_needed: bool) { - jet::check_lock_time(param::START_TIME); - - let target_asset_input_index: u32 = 0; - - let burn_grantor_output_index: u32 = match is_change_needed { - true => 1, - false => 0, - }; - - let expected_current_script_hash: u256 = get_input_script_hash(target_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, asset_amount, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset and grantor token amounts are correct - divmod_eq(asset_amount, param::SETTLEMENT_PER_CONTRACT, grantor_token_amount_to_burn); - - // Burn grantor token - ensure_output_is_op_return(burn_grantor_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); -} - -/* - * Expiry Path - */ -fn expiry_path(grantor_token_amount_to_burn: u64, collateral_amount: u64, is_change_needed: bool) { - jet::check_lock_time(param::EXPIRY_TIME); - - let collateral_input_index: u32 = 0; - - let burn_grantor_output_index: u32 = match is_change_needed { - true => 1, - false => 0, - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_current_script_hash, is_change_needed); - - // Ensure collateral amount is correct - divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, grantor_token_amount_to_burn); - - // Burn grantor token - ensure_output_is_op_return(burn_grantor_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); -} - -fn main() { - match witness::PATH { - Left(left_or_right: Either<(u64, u256, u256, u256, u256, u256, u256, u256, u256), Either<(bool, u64, u64, u64), (bool, u64, u64)>>) => match left_or_right { - Left(params: (u64, u256, u256, u256, u256, u256, u256, u256, u256)) => { - let (expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vbf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf): (u64, u256, u256, u256, u256, u256, u256, u256, u256) = params; - funding_path( - expected_asset_amount, - input_option_abf, input_option_vbf, - input_grantor_abf, input_grantor_vbf, - output_option_abf, output_option_vbf, - output_grantor_abf, output_grantor_vbf - ); - }, - Right(exercise_or_settlement: Either<(bool, u64, u64, u64), (bool, u64, u64)>) => match exercise_or_settlement { - Left(params: (bool, u64, u64, u64)) => { - let (is_change_needed, amount_to_burn, collateral_amount, asset_amount): (bool, u64, u64, u64) = dbg!(params); - exercise_path(amount_to_burn, collateral_amount, asset_amount, is_change_needed) - }, - Right(params: (bool, u64, u64)) => { - let (is_change_needed, amount_to_burn, asset_amount): (bool, u64, u64) = dbg!(params); - settlement_path(amount_to_burn, asset_amount, is_change_needed) - }, - }, - }, - Right(left_or_right: Either<(bool, u64, u64), (bool, u64, u64)>) => match left_or_right { - Left(params: (bool, u64, u64)) => { - let (is_change_needed, grantor_token_amount_to_burn, collateral_amount): (bool, u64, u64) = params; - expiry_path(grantor_token_amount_to_burn, collateral_amount, is_change_needed) - }, - Right(params: (bool, u64, u64)) => { - let (is_change_needed, amount_to_burn, collateral_amount): (bool, u64, u64) = params; - cancellation_path(amount_to_burn, collateral_amount, is_change_needed) - }, - }, - } -} diff --git a/crates/simplex/tests/ui/simple_storage.rs b/crates/simplex/tests/ui/simple_storage.rs index b5be89b..ea1a329 100644 --- a/crates/simplex/tests/ui/simple_storage.rs +++ b/crates/simplex/tests/ui/simple_storage.rs @@ -1,6 +1,5 @@ -use simplex_macros::*; -use simplex_sdk::witness::WitnessTrait; -use simplex_sdk::arguments::ArgumentsTrait; +use simplex::simplex_macros::*; +use simplex::simplex_sdk::witness::{WitnessTrait, ArgumentsTrait}; include_simf!("../../../../crates/simplex/tests/ui/simple_storage.simf"); @@ -21,4 +20,4 @@ fn main() -> Result<(), String> { assert_eq!(original_arguments, recovered_witness); Ok(()) -} \ No newline at end of file +} diff --git a/crates/simplex/tests/ui/simple_storage.simf b/crates/simplex/tests/ui/simple_storage.simf deleted file mode 100644 index 7ae6c41..0000000 --- a/crates/simplex/tests/ui/simple_storage.simf +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Simple Storage Program for Liquid - * - * Only the owner of the storage can modify the value. - * - * ==== IMPORTANT ==== - * - * Based on the following resources: - * https://github.com/ElementsProject/elements/blob/master/src/consensus/amount.h - * https://github.com/ElementsProject/rust-elements/blob/f6ffc7800df14b81c0f5ae1c94368a78b99612b9/src/blind.rs#L471 - * - * The maximum allowed amount is 2,100,000,000,000,000 - * (i.e., 21,000,000 × 10^8), which is approximately 51 bits. - */ - -fn checksig(pk: Pubkey, sig: Signature) { - let msg: u256 = jet::sig_all_hash(); - jet::bip_0340_verify((pk, msg), sig); -} - -fn ensure_current_index_eq(expected_index: u32){ - assert!(jet::eq_32(jet::current_index(), expected_index)); -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn ensure_output_is_op_return(index: u32) { - match jet::output_null_datum(index, 0) { - Some(entry: Option>>) => (), - None => panic!(), - } -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } -fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } - -fn increment_by(index: u32, amount: u32) -> u32 { - let (carry, result): (bool, u32) = jet::add_32(index, amount); - ensure_zero_bit(carry); - result -} - -fn enforce_stage_checks(index: u32, new_value: u64) { - ensure_input_and_output_script_hash_eq(index); - - let (asset_bits, old_value): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(asset_bits, param::SLOT_ID)); - - ensure_output_asset_with_amount_eq(index, param::SLOT_ID, new_value); - - match jet::lt_64(new_value, old_value) { - // burn - true => { - let burn_output_index: u32 = increment_by(index, 1); - - let (carry, amount_to_burn): (bool, u64) = jet::subtract_64(old_value, new_value); - ensure_zero_bit(carry); - - ensure_output_is_op_return(burn_output_index); - ensure_output_asset_with_amount_eq(burn_output_index, param::SLOT_ID, amount_to_burn); - }, - // mint - false => { - let reissuance_output_index: u32 = increment_by(index, 1); - ensure_input_and_output_script_hash_eq(reissuance_output_index); - }, - }; -} - -fn main() { - let index: u32 = 0; - enforce_stage_checks(index, witness::NEW_VALUE); - - checksig(param::USER, witness::USER_SIGNATURE) -} diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs index 59f3076..9cd42e8 100644 --- a/crates/test/src/error.rs +++ b/crates/test/src/error.rs @@ -1,6 +1,8 @@ +use std::io; + use electrsd::electrum_client::bitcoin::hex::HexToArrayError; + use simplex_provider::ExplorerError; -use std::io; #[derive(thiserror::Error, Debug)] pub enum TestError { diff --git a/crates/test/src/testing/rpc_provider.rs b/crates/test/src/testing/rpc_provider.rs index c1cb7a7..afd387e 100644 --- a/crates/test/src/testing/rpc_provider.rs +++ b/crates/test/src/testing/rpc_provider.rs @@ -1,19 +1,24 @@ -use crate::{ElementsdParams, TestError, common}; +use std::collections::HashMap; +use std::path::Path; +use std::str::FromStr; + use bitcoind::bitcoincore_rpc::bitcoin; + use electrsd::bitcoind; use electrsd::bitcoind::bitcoincore_rpc::jsonrpc::serde_json::Value; use electrsd::bitcoind::bitcoincore_rpc::{Auth, Client}; use electrsd::bitcoind::{BitcoinD, Conf}; -pub use simplex_provider::elements_rpc::*; -use simplex_sdk::constants::SimplicityNetwork; -use simplex_sdk::error::SimplexError; -use simplex_sdk::provider::ProviderSync; + use simplicityhl::elements::Transaction; use simplicityhl::elements::hex::ToHex; -use simplicityhl::elements::{Address, AssetId, BlockHash, Txid}; -use std::collections::HashMap; -use std::path::Path; -use std::str::FromStr; +use simplicityhl::elements::{Address, AssetId, BlockHash, OutPoint, Script, TxOut, Txid}; + +pub use simplex_provider::elements_rpc::*; + +use simplex_sdk::constants::SimplicityNetwork; +use simplex_sdk::provider::{ProviderError, ProviderTrait}; + +use crate::{ElementsdParams, TestError, common}; pub enum TestClientProvider { ConfiguredNode { node: BitcoinD, network: SimplicityNetwork }, @@ -96,24 +101,36 @@ pub struct TestRpcProvider { provider: TestClientProvider, } -impl ProviderSync for TestRpcProvider { - fn broadcast_transaction(&self, tx: &Transaction) -> Result { +impl ProviderTrait for TestRpcProvider { + fn broadcast_transaction(&self, tx: &Transaction) -> Result { use simplicityhl::simplicity::elements::encode; let tx_hex = encode::serialize_hex(tx); self.sendrawtransaction(&tx_hex) - .map_err(|e| SimplexError::RpcExecution(e.to_string())) + .map_err(|e| ProviderError::Request(e.to_string())) } - fn fetch_fee_estimates(&self) -> Result, SimplexError> { + fn wait(&self, txid: &Txid) -> Result<(), ProviderError> { + todo!() + } + + fn fetch_fee_estimates(&self) -> Result, ProviderError> { // Todo: search for appropriate endpoint let mut map = HashMap::new(); map.insert("".to_string(), 0.1); Ok(map) } - fn fetch_transaction(&self, txid: &Txid) -> Result { + fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError> { + todo!() + } + + fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError> { + todo!() + } + + fn fetch_transaction(&self, txid: &Txid) -> Result { self.gettransaction(&txid) - .map_err(|e| SimplexError::RpcExecution(e.to_string())) + .map_err(|e| ProviderError::Request(e.to_string())) } } diff --git a/crates/user/Cargo.toml b/crates/user/Cargo.toml index 5b42d86..3381189 100644 --- a/crates/user/Cargo.toml +++ b/crates/user/Cargo.toml @@ -11,5 +11,6 @@ workspace = true [dependencies] simplex-sdk = { workspace = true } +simplicityhl = { workspace = true } thiserror = "2" diff --git a/crates/user/src/lib.rs b/crates/user/src/lib.rs deleted file mode 100644 index 7f63866..0000000 --- a/crates/user/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -#[cfg(test)] -mod tests { - use simplex_sdk::constants::DUMMY_SIGNATURE; - use simplex_sdk::presets::p2pk::p2pk_build::P2PKWitness; - use simplex_sdk::signer::Signer; - - #[test] - #[ignore] - fn main() { - let signer = Signer::from_seed( - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - .as_bytes() - .try_into() - .unwrap(), - ) - .unwrap(); - - let witness = P2PKWitness { - signature: DUMMY_SIGNATURE, - }; - - // let arguments = P2PKArguments { - // public_key: signer.public_key(), - // }; - - // let p2pk = P2PK::new(&tr_unspendable_key(), &arguments); - } -} diff --git a/crates/user/src/main.rs b/crates/user/src/main.rs new file mode 100644 index 0000000..f82a5e8 --- /dev/null +++ b/crates/user/src/main.rs @@ -0,0 +1,96 @@ +use simplicityhl::elements::{Script, Txid}; + +use simplex_sdk::presets::{P2PK, P2PKArguments, P2PKWitness}; + +use simplex_sdk::constants::{DUMMY_SIGNATURE, SimplicityNetwork}; +use simplex_sdk::provider::{EsploraProvider, ProviderTrait}; +use simplex_sdk::signer::Signer; +use simplex_sdk::transaction::{FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature}; +use simplex_sdk::utils::tr_unspendable_key; + +const ESPLORA_URL: &str = "https://blockstream.info/liquidtestnet/api"; + +fn get_p2pk(signer: &Signer) -> (P2PK, Script) { + let arguments = P2PKArguments { + public_key: signer.get_schnorr_public_key().unwrap().serialize(), + }; + + let p2pk = P2PK::new(tr_unspendable_key(), arguments); + let p2pk_script = p2pk + .get_program() + .get_script_pubkey(SimplicityNetwork::LiquidTestnet) + .unwrap(); + + (p2pk, p2pk_script) +} + +fn spend_p2wpkh(signer: &Signer, provider: &EsploraProvider) -> Txid { + let (_, p2pk_script) = get_p2pk(&signer); + + let mut ft = FinalTransaction::new(SimplicityNetwork::LiquidTestnet); + + ft.add_output(PartialOutput::new( + p2pk_script.clone(), + 50, + SimplicityNetwork::LiquidTestnet.policy_asset(), + )); + + let (tx, _) = signer.finalize(&ft, 1).unwrap(); + let res = provider.broadcast_transaction(&tx).unwrap(); + + println!("Broadcast: {}", res); + + res +} + +fn spend_p2pk(signer: &Signer, provider: &EsploraProvider) -> Txid { + let (p2pk, p2pk_script) = get_p2pk(&signer); + + let mut p2pk_utxos = provider.fetch_scripthash_utxos(&p2pk_script).unwrap(); + + p2pk_utxos.retain(|el| el.1.asset.explicit().unwrap() == SimplicityNetwork::LiquidTestnet.policy_asset()); + + let mut ft = FinalTransaction::new(SimplicityNetwork::LiquidTestnet); + + let witness = P2PKWitness { + signature: DUMMY_SIGNATURE, + }; + + ft.add_program_input( + PartialInput::new(p2pk_utxos[0].0.clone(), p2pk_utxos[0].1.clone()), + ProgramInput::new(Box::new(p2pk.get_program().clone()), Box::new(witness.clone())), + RequiredSignature::Witness("SIGNATURE".to_string()), + ) + .unwrap(); + + let (tx, _) = signer.finalize(&ft, 1).unwrap(); + let res = provider.broadcast_transaction(&tx).unwrap(); + + println!("Broadcast: {}", res); + + res +} + +fn main() { + let provider = EsploraProvider::new(ESPLORA_URL.to_string()); + let signer = Signer::new( + "exist carry drive collect lend cereal occur much tiger just involve mean", + Box::new(provider.clone()), + SimplicityNetwork::LiquidTestnet, + ) + .unwrap(); + + let tx = spend_p2wpkh(&signer, &provider); + + provider.wait(&tx).unwrap(); + + println!("Confirmed"); + + let tx = spend_p2pk(&signer, &provider); + + provider.wait(&tx).unwrap(); + + println!("Confirmed"); + + println!("OK"); +} diff --git a/example.config.toml b/example.config.toml deleted file mode 100644 index c90b221..0000000 --- a/example.config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[network] -name = "testnet" \ No newline at end of file diff --git a/examples/test_usage/Cargo.toml b/example/Cargo.toml similarity index 85% rename from examples/test_usage/Cargo.toml rename to example/Cargo.toml index 68c46e4..7d1c449 100644 --- a/examples/test_usage/Cargo.toml +++ b/example/Cargo.toml @@ -14,4 +14,4 @@ simplicityhl = { workspace = true } [dev-dependencies] anyhow = { version = "1.0.102" } -simplicityhl-core = { version = "0.4.2" } \ No newline at end of file +simplicityhl-core = { version = "0.4.2" } diff --git a/examples/options/Simplex.toml b/example/Simplex.toml similarity index 52% rename from examples/options/Simplex.toml rename to example/Simplex.toml index cccff48..e6080e5 100644 --- a/examples/options/Simplex.toml +++ b/example/Simplex.toml @@ -2,7 +2,7 @@ network = "liquidtestnet" [build] compile_simf = ["simf/*.simf"] -out_dir = "./src/program" +out_dir = "./src/artifacts" [tests] -elementsd_path = "../../assets/elementsd" \ No newline at end of file +elementsd_path = "../assets/elementsd" diff --git a/crates/simplex/examples/source_simf/options.simf b/example/simf/options.simf similarity index 100% rename from crates/simplex/examples/source_simf/options.simf rename to example/simf/options.simf diff --git a/crates/simplex/examples/source_simf/p2pk.simf b/example/simf/p2pk.simf similarity index 100% rename from crates/simplex/examples/source_simf/p2pk.simf rename to example/simf/p2pk.simf diff --git a/example/simplex b/example/simplex new file mode 100755 index 0000000..d414575 Binary files /dev/null and b/example/simplex differ diff --git a/example/src/lib.rs b/example/src/lib.rs new file mode 100644 index 0000000..8bb12f3 --- /dev/null +++ b/example/src/lib.rs @@ -0,0 +1,3 @@ +// mod artifacts; + +// pub use artifacts::*; diff --git a/examples/test_usage/tests/draft_test.rs b/example/tests/draft_test.rs similarity index 100% rename from examples/test_usage/tests/draft_test.rs rename to example/tests/draft_test.rs diff --git a/examples/options/Cargo.toml b/examples/options/Cargo.toml deleted file mode 100644 index 72ab800..0000000 --- a/examples/options/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "options" -version = "0.1.0" -license.workspace = true -edition.workspace = true - - -[lints] -workspace = true - -[dependencies] -simplex = { workspace = true } - -simplicityhl = { workspace = true } diff --git a/examples/options/simf/options.simf b/examples/options/simf/options.simf deleted file mode 100644 index e7da014..0000000 --- a/examples/options/simf/options.simf +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Options - * - * Important: Currently only the LBTC collateral is supported. - * - * Based on the https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf - * - * This contract implements cash-settled European-style options using covenant-locked collateral. - * - * Room for optimization: - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/2 (Use input asset to determine option covenent type) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/3 (Simplify match token_branch in funding_path.) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 (why batching is hard to implement) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/5 (Reduce Contract Parameters) - * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/21 (explains why funding is limited) - */ - -/// Assert: a == b * expected_q, via divmod -fn divmod_eq(a: u64, b: u64, expected_q: u64) { - let (q, r): (u64, u64) = jet::div_mod_64(a, b); - assert!(jet::eq_64(q, expected_q)); - assert!(jet::eq_64(r, 0)); -} - -fn get_output_script_hash(index: u32) -> u256 { - unwrap(jet::output_script_hash(index)) -} - -fn get_input_script_hash(index: u32) -> u256 { - unwrap(jet::input_script_hash(index)) -} - -fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { - let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (asset, amount): (Asset1, Amount1) = pair; - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - let amount: u64 = unwrap_right::<(u1, u256)>(amount); - (asset_bits, amount) -} - -fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } -fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } - -fn increment_by(index: u32, amount: u32) -> u32 { - let (carry, result): (bool, u32) = jet::add_32(index, amount); - ensure_zero_bit(carry); - result -} - -fn ensure_input_and_output_script_hash_eq(index: u32) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); -} - -fn ensure_output_is_op_return(index: u32) { - match jet::output_null_datum(index, 0) { - Some(entry: Option>>) => (), - None => panic!(), - } -} - -fn ensure_input_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::input_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_eq(index: u32, expected_bits: u256) { - let asset: Asset1 = unwrap(jet::output_asset(index)); - let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); - assert!(jet::eq_256(asset_bits, expected_bits)); -} - -fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { - let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); - assert!(jet::eq_256(asset, expected_bits)); - assert!(jet::eq_64(amount, expected_amount)); -} - -fn ensure_input_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected)); -} - -fn ensure_output_script_hash_eq(index: u32, expected: u256) { - assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected)); -} - -fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) { - let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index); - assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash)); - assert!(jet::eq_32(jet::current_index(), index)); - - match is_change_needed { - true => { - ensure_input_and_output_script_hash_eq(index); - - let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend); - ensure_zero_bit(carry); - ensure_output_asset_with_amount_eq(index, asset_id, collateral_change); - }, - false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)), - } -} - -fn check_y(expected_y: Fe, actual_y: Fe) { - match jet::eq_256(expected_y, actual_y) { - true => {}, - false => { - assert!(jet::eq_256(expected_y, jet::fe_negate(actual_y))); - } - }; -} - -fn ensure_input_and_output_reissuance_token_eq(index: u32) { - let (input_asset, input_amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); - let (output_asset, output_amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); - - match (input_asset) { - Left(in_conf: Point) => { - let (input_asset_parity, input_asset_x): (u1, u256) = in_conf; - let (output_asset_parity, output_asset_x): (u1, u256) = unwrap_left::(output_asset); - - assert!(jet::eq_1(input_asset_parity, output_asset_parity)); - assert!(jet::eq_256(input_asset_x, output_asset_x)); - }, - Right(in_expl: u256) => { - let out_expl: u256 = unwrap_right::(output_asset); - assert!(jet::eq_256(in_expl, out_expl)); - } - }; - - match (input_amount) { - Left(in_conf: Point) => { - let (input_amount_parity, input_amount_x): (u1, u256) = in_conf; - let (output_amount_parity, output_amount_x): (u1, u256) = unwrap_left::(output_amount); - - assert!(jet::eq_1(input_amount_parity, output_amount_parity)); - assert!(jet::eq_256(input_amount_x, output_amount_x)); - }, - Right(in_expl: u64) => { - let out_expl: u64 = unwrap_right::(output_amount); - assert!(jet::eq_64(in_expl, out_expl)); - } - }; -} - -// Verify that a reissuance token commitment matches the expected token ID using provided blinding factors. -// Reissuance tokens are confidential because, in Elements, -// the asset must be provided in blinded form in order to reissue tokens. -// https://github.com/BlockstreamResearch/simplicity-contracts/issues/21#issuecomment-3691599583 -fn verify_token_commitment(actual_asset: Asset1, actual_amount: Amount1, expected_token_id: u256, abf: u256, vbf: u256) { - match actual_asset { - Left(conf_token: Point) => { - let amount_scalar: u256 = 1; - let (actual_ax, actual_ay): Ge = unwrap(jet::decompress(conf_token)); - - let gej_point: Gej = (jet::hash_to_curve(expected_token_id), 1); - let asset_blind_point: Gej = jet::generate(abf); - - let asset_generator: Gej = jet::gej_add(gej_point, asset_blind_point); - let (ax, ay): Ge = unwrap(jet::gej_normalize(asset_generator)); - - assert!(jet::eq_256(actual_ax, ax)); - check_y(actual_ay, ay); - - // Check amount - let conf_val: Point = unwrap_left::(actual_amount); - let (actual_vx, actual_vy): Ge = unwrap(jet::decompress(conf_val)); - - let amount_part: Gej = jet::scale(amount_scalar, asset_generator); - let vbf_part: Gej = jet::generate(vbf); - - let value_generator: Gej = jet::gej_add(amount_part, vbf_part); - let (vx, vy): Ge = unwrap(jet::gej_normalize(value_generator)); - - assert!(jet::eq_256(actual_vx, vx)); - check_y(actual_vy, vy); - }, - Right(reissuance_token: u256) => { - let expected_amount: u64 = 1; - let actual_amount: u64 = unwrap_right::(actual_amount); - - assert!(jet::eq_64(expected_amount, actual_amount)); - assert!(jet::eq_256(reissuance_token, expected_token_id)); - } - }; -} - -fn verify_output_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { - let (asset, amount): (Asset1, Amount1) = unwrap(jet::output_amount(index)); - verify_token_commitment(asset, amount, expected_token_id, abf, vbf); -} - -fn verify_input_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) { - let (asset, amount): (Asset1, Amount1) = unwrap(jet::input_amount(index)); - verify_token_commitment(asset, amount, expected_token_id, abf, vbf); -} - -/* - * Funding Path - */ -fn funding_path( - expected_asset_amount: u64, - input_option_abf: u256, - input_option_vbf: u256, - input_grantor_abf: u256, - input_grantor_vbf: u256, - output_option_abf: u256, - output_option_vbf: u256, - output_grantor_abf: u256, - output_grantor_vbf: u256 -) { - ensure_input_and_output_script_hash_eq(0); - ensure_input_and_output_script_hash_eq(1); - - verify_input_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, input_option_abf, input_option_vbf); - verify_input_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, input_grantor_abf, input_grantor_vbf); - - verify_output_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, output_option_abf, output_option_vbf); - verify_output_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, output_grantor_abf, output_grantor_vbf); - - assert!(dbg!(jet::eq_256(get_output_script_hash(0), get_output_script_hash(1)))); - - assert!(jet::le_32(jet::current_index(), 1)); - - ensure_output_script_hash_eq(2, get_output_script_hash(0)); - - let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(2); - let option_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0)))); - let grantor_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1)))); - assert!(jet::eq_64(option_token_amount, grantor_token_amount)); - - divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, option_token_amount); - divmod_eq(expected_asset_amount, param::SETTLEMENT_PER_CONTRACT, option_token_amount); - - ensure_output_asset_with_amount_eq(2, param::COLLATERAL_ASSET_ID, collateral_amount); - ensure_output_asset_with_amount_eq(3, param::OPTION_TOKEN_ASSET, option_token_amount); - ensure_output_asset_with_amount_eq(4, param::GRANTOR_TOKEN_ASSET, grantor_token_amount); -} - -/* - * Cancellation Path - */ -fn cancellation_path(amount_to_burn: u64, collateral_amount_to_withdraw: u64, is_change_needed: bool) { - let collateral_input_index: u32 = 0; - let option_input_index: u32 = 1; - let grantor_input_index: u32 = 2; - - let (burn_option_output_index, burn_grantor_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_withdraw, expected_current_script_hash, is_change_needed); - - // Burn option and grantor tokens - ensure_output_is_op_return(burn_option_output_index); - ensure_output_is_op_return(burn_grantor_output_index); - - ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, amount_to_burn); - ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, amount_to_burn); - - // Ensure returned collateral amount is correct - divmod_eq(collateral_amount_to_withdraw, param::COLLATERAL_PER_CONTRACT, amount_to_burn); -} - -/* - * Exercise Path - */ -fn exercise_path(option_amount_to_burn: u64, collateral_amount_to_get: u64, asset_amount_to_pay: u64, is_change_needed: bool) { - jet::check_lock_time(param::START_TIME); - - let collateral_input_index: u32 = 0; - - let (burn_option_output_index, asset_to_covenant_output_index): (u32, u32) = match is_change_needed { - true => (1, 2), - false => (0, 1), - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed); - - // Ensure collateral and asset amounts are correct - divmod_eq(collateral_amount_to_get, param::COLLATERAL_PER_CONTRACT, option_amount_to_burn); - divmod_eq(asset_amount_to_pay, param::SETTLEMENT_PER_CONTRACT, option_amount_to_burn); - - // Burn option token - ensure_output_is_op_return(burn_option_output_index); - ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, option_amount_to_burn); - - // Ensure settlement asset and script hash are correct - ensure_output_asset_with_amount_eq(asset_to_covenant_output_index, param::SETTLEMENT_ASSET_ID, asset_amount_to_pay); - ensure_output_script_hash_eq(asset_to_covenant_output_index, expected_current_script_hash); -} - -/* - * Settlement Path - */ -fn settlement_path(grantor_token_amount_to_burn: u64, asset_amount: u64, is_change_needed: bool) { - jet::check_lock_time(param::START_TIME); - - let target_asset_input_index: u32 = 0; - - let burn_grantor_output_index: u32 = match is_change_needed { - true => 1, - false => 0, - }; - - let expected_current_script_hash: u256 = get_input_script_hash(target_asset_input_index); - - // Check and ensure settlement asset change - ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, asset_amount, expected_current_script_hash, is_change_needed); - - // Ensure settlement asset and grantor token amounts are correct - divmod_eq(asset_amount, param::SETTLEMENT_PER_CONTRACT, grantor_token_amount_to_burn); - - // Burn grantor token - ensure_output_is_op_return(burn_grantor_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); -} - -/* - * Expiry Path - */ -fn expiry_path(grantor_token_amount_to_burn: u64, collateral_amount: u64, is_change_needed: bool) { - jet::check_lock_time(param::EXPIRY_TIME); - - let collateral_input_index: u32 = 0; - - let burn_grantor_output_index: u32 = match is_change_needed { - true => 1, - false => 0, - }; - - let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index); - - // Check and ensure collateral change - ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_current_script_hash, is_change_needed); - - // Ensure collateral amount is correct - divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, grantor_token_amount_to_burn); - - // Burn grantor token - ensure_output_is_op_return(burn_grantor_output_index); - ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn); -} - -fn main() { - match witness::PATH { - Left(left_or_right: Either<(u64, u256, u256, u256, u256, u256, u256, u256, u256), Either<(bool, u64, u64, u64), (bool, u64, u64)>>) => match left_or_right { - Left(params: (u64, u256, u256, u256, u256, u256, u256, u256, u256)) => { - let (expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vbf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf): (u64, u256, u256, u256, u256, u256, u256, u256, u256) = params; - funding_path( - expected_asset_amount, - input_option_abf, input_option_vbf, - input_grantor_abf, input_grantor_vbf, - output_option_abf, output_option_vbf, - output_grantor_abf, output_grantor_vbf - ); - }, - Right(exercise_or_settlement: Either<(bool, u64, u64, u64), (bool, u64, u64)>) => match exercise_or_settlement { - Left(params: (bool, u64, u64, u64)) => { - let (is_change_needed, amount_to_burn, collateral_amount, asset_amount): (bool, u64, u64, u64) = dbg!(params); - exercise_path(amount_to_burn, collateral_amount, asset_amount, is_change_needed) - }, - Right(params: (bool, u64, u64)) => { - let (is_change_needed, amount_to_burn, asset_amount): (bool, u64, u64) = dbg!(params); - settlement_path(amount_to_burn, asset_amount, is_change_needed) - }, - }, - }, - Right(left_or_right: Either<(bool, u64, u64), (bool, u64, u64)>) => match left_or_right { - Left(params: (bool, u64, u64)) => { - let (is_change_needed, grantor_token_amount_to_burn, collateral_amount): (bool, u64, u64) = params; - expiry_path(grantor_token_amount_to_burn, collateral_amount, is_change_needed) - }, - Right(params: (bool, u64, u64)) => { - let (is_change_needed, amount_to_burn, collateral_amount): (bool, u64, u64) = params; - cancellation_path(amount_to_burn, collateral_amount, is_change_needed) - }, - }, - } -} diff --git a/examples/options/src/lib.rs b/examples/options/src/lib.rs deleted file mode 100644 index 2dc508d..0000000 --- a/examples/options/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -mod program; - -pub use program::*; diff --git a/examples/options/src/program/mod.rs b/examples/options/src/program/mod.rs deleted file mode 100644 index 66dd779..0000000 --- a/examples/options/src/program/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod options; diff --git a/examples/options/src/program/options.rs b/examples/options/src/program/options.rs deleted file mode 100644 index 619b668..0000000 --- a/examples/options/src/program/options.rs +++ /dev/null @@ -1,22 +0,0 @@ -use simplex::simplex_macros::include_simf; -use simplex::simplex_sdk::arguments::ArgumentsTrait; -use simplex::simplex_sdk::program::Program; -use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; -pub struct OptionsProgram<'a> { - program: Program<'a>, -} -impl<'a> OptionsProgram<'a> { - pub const SOURCE: &'static str = derived_options::OPTIONS_CONTRACT_SOURCE; - 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 - } - pub fn get_program_mut(&mut self) -> &mut Program<'a> { - &mut self.program - } -} -include_simf!("simf/options.simf"); diff --git a/examples/p2pk/Cargo.toml b/examples/p2pk/Cargo.toml deleted file mode 100644 index 599c870..0000000 --- a/examples/p2pk/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "p2pk" -version = "0.1.0" -license.workspace = true -edition.workspace = true - - -[lints] -workspace = true - - -[dependencies] -simplex = { path = "../../crates/simplex" } - -simplicityhl = { git = "https://github.com/ikripaka/SimplicityHL/", branch = "feature/rich-params" } diff --git a/examples/p2pk/Simplex.toml b/examples/p2pk/Simplex.toml deleted file mode 100644 index 1ebb2e9..0000000 --- a/examples/p2pk/Simplex.toml +++ /dev/null @@ -1,8 +0,0 @@ -network = "liquidtestnet" - -[build] -compile_simf = ["simf/*.simf"] -out_dir = "./out_dir" - -[tests] -elementsd_path = "../../assets/elementsd" \ No newline at end of file diff --git a/examples/p2pk/out_dir/p2pk.rs b/examples/p2pk/out_dir/p2pk.rs deleted file mode 100644 index e5a7b2d..0000000 --- a/examples/p2pk/out_dir/p2pk.rs +++ /dev/null @@ -1,25 +0,0 @@ -use simplex::simplex_macros::include_simf; -use simplex::simplex_sdk::arguments::ArgumentsTrait; -use simplex::simplex_sdk::program::Program; -use simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; -pub struct P2pkProgram<'a> { - program: Program<'a>, -} -impl<'a> P2pkProgram<'a> { - pub const SOURCE: &'static str = derived_p2pk::P2PK_CONTRACT_SOURCE; - 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 - } - pub fn get_program_mut(&mut self) -> &mut Program<'a> { - &mut self.program - } -} -include_simf!("simf/p2pk.simf"); diff --git a/examples/p2pk/simf/p2pk.simf b/examples/p2pk/simf/p2pk.simf deleted file mode 100644 index db4f27c..0000000 --- a/examples/p2pk/simf/p2pk.simf +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - jet::bip_0340_verify((param::PUBLIC_KEY, jet::sig_all_hash()), witness::SIGNATURE) -} \ No newline at end of file diff --git a/examples/p2pk/src/lib.rs b/examples/p2pk/src/lib.rs deleted file mode 100644 index 3a3024c..0000000 --- a/examples/p2pk/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub use simplex; - -mod p2pk_program { - include!(concat!("../out_dir", "/p2pk.rs")); -} -pub use p2pk_program::*; diff --git a/examples/test_usage/Simplex.toml b/examples/test_usage/Simplex.toml deleted file mode 100644 index 675755b..0000000 --- a/examples/test_usage/Simplex.toml +++ /dev/null @@ -1,4 +0,0 @@ -network = "liquidtestnet" - -[tests] -elementsd_path = "../../assets/elementsd" \ No newline at end of file diff --git a/examples/test_usage/src/lib.rs b/examples/test_usage/src/lib.rs deleted file mode 100644 index 8b13789..0000000 --- a/examples/test_usage/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -