From 12c6c4161e015400860e91477697d888544cbcea Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 7 Mar 2026 07:51:01 +0200 Subject: [PATCH 1/7] contracts: pre lock: move witness sig out of branch --- .../contracts/src/pre_lock/build_witness.rs | 38 ++++++++++++------- .../src/pre_lock/source_simf/pre_lock.simf | 6 +-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/crates/contracts/src/pre_lock/build_witness.rs b/crates/contracts/src/pre_lock/build_witness.rs index 1ab636f..7ddf12a 100644 --- a/crates/contracts/src/pre_lock/build_witness.rs +++ b/crates/contracts/src/pre_lock/build_witness.rs @@ -11,7 +11,7 @@ use simplicityhl::{ pub enum PreLockBranch<'a> { // Left(()) LendingCreation, - // Right(Signature) + // Right(()) PreLockCancellation { cancellation_signature: &'a bitcoin::secp256k1::schnorr::Signature, }, @@ -24,24 +24,34 @@ pub enum PreLockBranch<'a> { #[must_use] pub fn build_pre_lock_witness(branch: PreLockBranch) -> WitnessValues { let lending_creation = ResolvedType::parse_from_str("()").unwrap(); - let pre_lock_cancellation = ResolvedType::parse_from_str("Signature").unwrap(); + let pre_lock_cancellation = ResolvedType::parse_from_str("()").unwrap(); let path_type = ResolvedType::either(lending_creation, pre_lock_cancellation); - let branch_str = match branch { + let path = match branch { PreLockBranch::LendingCreation => "Left(())".to_string(), - PreLockBranch::PreLockCancellation { - cancellation_signature, - } => { - format!( - "Right({})", - Value::byte_array(cancellation_signature.serialize()), - ) - } + PreLockBranch::PreLockCancellation { .. } => "Right(())".to_string(), }; - simplicityhl::WitnessValues::from(HashMap::from([( + let mut values = HashMap::from([( WitnessName::from_str_unchecked("PATH"), - simplicityhl::Value::parse_from_str(&branch_str, &path_type).unwrap(), - )])) + simplicityhl::Value::parse_from_str(&path, &path_type).unwrap(), + )]); + + if let PreLockBranch::PreLockCancellation { + cancellation_signature, + } = branch + { + values.insert( + WitnessName::from_str_unchecked("CANCELLATION_SIGNATURE"), + Value::byte_array(cancellation_signature.serialize()), + ); + } else { + values.insert( + WitnessName::from_str_unchecked("CANCELLATION_SIGNATURE"), + Value::byte_array([0; 64]), + ); + } + + simplicityhl::WitnessValues::from(values) } diff --git a/crates/contracts/src/pre_lock/source_simf/pre_lock.simf b/crates/contracts/src/pre_lock/source_simf/pre_lock.simf index e692547..5ef02df 100644 --- a/crates/contracts/src/pre_lock/source_simf/pre_lock.simf +++ b/crates/contracts/src/pre_lock/source_simf/pre_lock.simf @@ -225,8 +225,8 @@ fn main() { Left(params: ()) => { create_lending_path(); }, - Right(cancellation_signature: Signature) => { - cancel_pre_lock_path(cancellation_signature); + Right(params: ()) => { + cancel_pre_lock_path(witness::CANCELLATION_SIGNATURE); } } -} \ No newline at end of file +} From 33d6f8d49821a4a0db3138135092a02ab0e89966 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 7 Mar 2026 07:51:26 +0200 Subject: [PATCH 2/7] integration tests: add wallet-abi integration tests --- Cargo.lock | 1343 ++++++++++- crates/contracts/Cargo.toml | 5 + .../support/wallet_abi_asset_auth_support.rs | 171 ++ .../tests/support/wallet_abi_common.rs | 251 ++ .../support/wallet_abi_lending_support.rs | 2099 +++++++++++++++++ .../support/wallet_abi_script_auth_support.rs | 179 ++ .../contracts/tests/wallet_abi_asset_auth.rs | 95 + .../tests/wallet_abi_lending_protocol.rs | 149 ++ .../contracts/tests/wallet_abi_script_auth.rs | 85 + 9 files changed, 4340 insertions(+), 37 deletions(-) create mode 100644 crates/contracts/tests/support/wallet_abi_asset_auth_support.rs create mode 100644 crates/contracts/tests/support/wallet_abi_common.rs create mode 100644 crates/contracts/tests/support/wallet_abi_lending_support.rs create mode 100644 crates/contracts/tests/support/wallet_abi_script_auth_support.rs create mode 100644 crates/contracts/tests/wallet_abi_asset_auth.rs create mode 100644 crates/contracts/tests/wallet_abi_lending_protocol.rs create mode 100644 crates/contracts/tests/wallet_abi_script_auth.rs diff --git a/Cargo.lock b/Cargo.lock index c524862..bcbeb51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,85 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "age" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf640be7658959746f1f0f2faab798f6098a9436a8e18e148d18bc9875e13c4b" +dependencies = [ + "age-core", + "base64 0.21.7", + "bech32 0.9.1", + "chacha20poly1305", + "cookie-factory", + "hmac", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "nom", + "pin-project", + "rand 0.8.5", + "rust-embed", + "scrypt", + "sha2", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" +dependencies = [ + "base64 0.21.7", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "io_tee", + "nom", + "rand 0.8.5", + "secrecy", + "sha2", +] + [[package]] name = "ahash" version = "0.8.12" @@ -95,6 +174,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -115,7 +203,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -223,6 +311,12 @@ dependencies = [ "bitcoin_hashes", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -241,6 +335,21 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bech32" version = "0.11.1" @@ -267,6 +376,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" @@ -274,7 +396,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", - "bech32", + "base64 0.21.7", + "bech32 0.11.1", "bitcoin-internals", "bitcoin-io", "bitcoin-units", @@ -282,6 +405,7 @@ dependencies = [ "hex-conservative", "hex_lit", "secp256k1", + "serde", ] [[package]] @@ -289,6 +413,9 @@ name = "bitcoin-internals" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] [[package]] name = "bitcoin-io" @@ -309,6 +436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ "bitcoin-internals", + "serde", ] [[package]] @@ -319,6 +447,44 @@ checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoind" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6620b7c942dbe28cc49c21d95e792feb9ffd95a093205e7875ccfa69c2925" +dependencies = [ + "anyhow", + "bitcoincore-rpc", + "log", + "tempfile", + "which", ] [[package]] @@ -345,6 +511,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bmp-monochrome" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567" + +[[package]] +name = "bollard-stubs" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2f2e73fffe9455141e170fb9c1feb0ac521ec7e7dcd47a7cab72a658490fb8" +dependencies = [ + "chrono", + "serde", + "serde_with", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -399,6 +582,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.43" @@ -406,10 +613,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "4.5.54" @@ -429,7 +650,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -441,7 +662,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -518,7 +739,7 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml", + "toml 0.9.11+spec-1.1.0", "winnow", "yaml-rust2", ] @@ -572,6 +793,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -668,9 +898,80 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + [[package]] name = "der" version = "0.7.10" @@ -711,7 +1012,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -744,15 +1045,71 @@ dependencies = [ "serde", ] +[[package]] +name = "electrsd" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91435161fb2ad5098e7ac7a4b793bf9c34723b0208a3fcf6f33707489e771396" +dependencies = [ + "bitcoind", + "electrum-client", + "log", + "nix", + "which", +] + +[[package]] +name = "electrum-client" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0bd443023f9f5c4b7153053721939accc7113cbdf810a024434eed454b3db1" +dependencies = [ + "bitcoin", + "byteorder", + "libc", + "log", + "rustls 0.23.36", + "serde", + "serde_json", + "webpki-roots 0.25.4", + "winapi", +] + [[package]] name = "elements" version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" dependencies = [ - "bech32", + "bech32 0.11.1", + "bitcoin", + "secp256k1-zkp", + "serde", + "serde_json", +] + +[[package]] +name = "elements" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d562b364c5d2aced40b01b3f73fc968311787e6813957593d4ffa94cd8733e3" +dependencies = [ + "bech32 0.11.1", "bitcoin", "secp256k1-zkp", + "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 0.25.2", + "miniscript", + "serde", ] [[package]] @@ -764,6 +1121,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -781,6 +1161,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -803,12 +1193,77 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "find-msvc-tools" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "flume" version = "0.11.1" @@ -857,6 +1312,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -901,6 +1371,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -919,8 +1400,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -958,6 +1441,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1178,6 +1670,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 1.0.6", ] [[package]] @@ -1205,6 +1698,72 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot 0.12.5", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" +dependencies = [ + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error2", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.114", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1310,6 +1869,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1341,6 +1906,15 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -1350,6 +1924,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + [[package]] name = "ipnet" version = "2.11.0" @@ -1387,6 +1986,30 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "jni" version = "0.21.1" @@ -1440,6 +2063,18 @@ dependencies = [ "serde", ] +[[package]] +name = "jsonrpc" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" +dependencies = [ + "base64 0.13.1", + "minreq", + "serde", + "serde_json", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1479,12 +2114,17 @@ dependencies = [ "cargo-husky", "contracts", "hex", + "lwk_common", + "lwk_simplicity", + "lwk_wollet", + "minreq", "modular-bitfield", "ring", "sha2", "simplicityhl", "simplicityhl-core", "thiserror 2.0.18", + "tokio", ] [[package]] @@ -1497,7 +2137,7 @@ dependencies = [ "config", "hex", "lending-contracts", - "reqwest", + "reqwest 0.13.1", "serde", "serde_json", "simplicityhl", @@ -1546,6 +2186,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.1" @@ -1573,6 +2225,120 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lwk_common" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7ab9af7c3891474f4592e5ff62d8a5c2000d37b4caa9b4020c4d666d43284e4" +dependencies = [ + "aes-gcm-siv", + "base64 0.21.7", + "elements 0.25.2", + "elements-miniscript", + "getrandom 0.2.17", + "qr_code", + "rand 0.8.5", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "lwk_containers" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb8dd8521dc2613c494b5ad4856d864499b694a83e3bd33c4aec2c30e084462" +dependencies = [ + "elements 0.25.2", + "rand 0.8.5", + "tempfile", + "testcontainers", +] + +[[package]] +name = "lwk_signer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15fc0606348df486e7fcc914aad91c6181e0f46da56ec336b1db99d7209a4abe" +dependencies = [ + "base64 0.21.7", + "bip39", + "elements-miniscript", + "lwk_common", + "thiserror 1.0.69", +] + +[[package]] +name = "lwk_simplicity" +version = "0.15.0" +source = "git+https://github.com/KyrylR/lwk?rev=751dd96#751dd9687e3649cbd71b916afc512fd1560a985f" +dependencies = [ + "log", + "lwk_common", + "lwk_signer", + "lwk_test_util", + "lwk_wollet", + "serde", + "serde_json", + "simplicityhl", + "thiserror 2.0.18", + "tokio", + "uuid", +] + +[[package]] +name = "lwk_test_util" +version = "0.15.0" +source = "git+https://github.com/KyrylR/lwk?rev=751dd96#751dd9687e3649cbd71b916afc512fd1560a985f" +dependencies = [ + "bip39", + "electrsd", + "elements-miniscript", + "env_logger", + "log", + "lwk_common", + "lwk_containers", + "pulldown-cmark", + "rand 0.8.5", + "reqwest 0.12.28", + "serde_json", + "tempfile", +] + +[[package]] +name = "lwk_wollet" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaafa62c6b51594ff9f7f899d5374ec97d1bf4e9f279787dba745d716fa560d7" +dependencies = [ + "aes-gcm-siv", + "age", + "base64 0.21.7", + "bip39", + "electrum-client", + "elements 0.25.2", + "elements 0.26.1", + "elements-miniscript", + "futures", + "fxhash", + "idna", + "js-sys", + "log", + "lwk_common", + "once_cell", + "rand 0.8.5", + "regex-lite", + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "url", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1604,19 +2370,34 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniscript" version = "12.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" dependencies = [ - "bech32", + "bech32 0.11.1", "bitcoin", ] @@ -1628,6 +2409,8 @@ checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" dependencies = [ "rustls 0.21.12", "rustls-webpki 0.101.7", + "serde", + "serde_json", "webpki-roots 0.25.4", ] @@ -1660,7 +2443,31 @@ checksum = "59b43b4fd69e3437618106f7754f34021b831a514f9e1a98ae863cabcd8d8dad" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", ] [[package]] @@ -1736,6 +2543,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -1812,6 +2625,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1857,7 +2680,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1870,6 +2693,26 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1909,6 +2752,44 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1928,18 +2809,61 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "zerocopy", + "bitflags 2.10.0", + "getopts", + "memchr", + "unicase", ] [[package]] -name = "proc-macro2" -version = "1.0.105" +name = "qr_code" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "43d2564aae5faaf3acb512b35b8bcb9a298d9d8c72d181c598691d800ee78a00" dependencies = [ - "unicode-ident", + "bmp-monochrome", ] [[package]] @@ -1953,7 +2877,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.36", "socket2", "thiserror 2.0.18", @@ -1974,7 +2898,7 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.36", "rustls-pki-types", "slab", @@ -2122,12 +3046,61 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.36", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http 0.6.8", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.6", +] + [[package]] name = "reqwest" version = "0.13.1" @@ -2214,6 +3187,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.114", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -2224,12 +3231,53 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.21.12" @@ -2249,6 +3297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -2340,6 +3389,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2373,6 +3431,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -2392,6 +3461,7 @@ dependencies = [ "bitcoin_hashes", "rand 0.8.5", "secp256k1-sys", + "serde", ] [[package]] @@ -2413,6 +3483,7 @@ dependencies = [ "rand 0.8.5", "secp256k1", "secp256k1-zkp-sys", + "serde", ] [[package]] @@ -2425,6 +3496,15 @@ dependencies = [ "secp256k1-sys", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "security-framework" version = "3.5.1" @@ -2448,6 +3528,27 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.2.2", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -2487,7 +3588,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2535,6 +3636,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2591,7 +3714,7 @@ dependencies = [ "bitcoin", "bitcoin_hashes", "byteorder", - "elements", + "elements 0.25.2", "getrandom 0.2.17", "ghost-cell", "hex-conservative", @@ -2612,9 +3735,9 @@ dependencies = [ [[package]] name = "simplicityhl" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79c6da84ac06f1162607b83508e83a58dc19049a2a3e006b44926bee3841bc2" +checksum = "3aa7477fc9bfef4cc53ae969db00539f0e67af38156822ac79662513d04f6fee" dependencies = [ "base64 0.21.7", "clap", @@ -2763,7 +3886,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.114", ] [[package]] @@ -2786,7 +3909,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.114", "tokio", "url", ] @@ -2923,6 +4046,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -2935,6 +4064,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.114" @@ -2963,7 +4103,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2987,6 +4127,36 @@ dependencies = [ "libc", ] +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "testcontainers" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e2b1567ca8a2b819ea7b28c92be35d9f76fb9edb214321dcc86eb96023d1f87" +dependencies = [ + "bollard-stubs", + "futures", + "hex", + "hmac", + "log", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3013,7 +4183,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -3024,7 +4194,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -3083,6 +4253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -3124,7 +4295,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -3161,6 +4332,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.9.11+spec-1.1.0" @@ -3276,7 +4456,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -3353,6 +4533,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + [[package]] name = "typeid" version = "1.0.3" @@ -3371,6 +4560,31 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -3404,6 +4618,22 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3562,7 +4792,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -3628,6 +4858,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "whoami" version = "1.6.1" @@ -3690,7 +4932,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -3701,7 +4943,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -4048,6 +5290,18 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "yaml-rust2" version = "0.10.4" @@ -4078,7 +5332,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] @@ -4099,7 +5353,7 @@ checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -4119,7 +5373,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] @@ -4128,6 +5382,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "zerotrie" @@ -4146,6 +5414,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -4159,7 +5428,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index 45e1695..4e85284 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -39,3 +39,8 @@ simplicity-contracts = { workspace = true } [dev-dependencies] anyhow = "1" cargo-husky = { workspace = true } +lwk_common = "0.15.0" +lwk_simplicity = { git = "https://github.com/KyrylR/lwk", rev = "751dd96", features = ["wallet_abi_test_utils"] } +lwk_wollet = { version = "0.15.0", default-features = false, features = ["electrum", "esplora"] } +minreq = "2.14.1" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] } diff --git a/crates/contracts/tests/support/wallet_abi_asset_auth_support.rs b/crates/contracts/tests/support/wallet_abi_asset_auth_support.rs new file mode 100644 index 0000000..4926ea2 --- /dev/null +++ b/crates/contracts/tests/support/wallet_abi_asset_auth_support.rs @@ -0,0 +1,171 @@ +use anyhow::{Context, Result, bail}; +use lending_contracts::asset_auth::{ + ASSET_AUTH_SOURCE, + build_arguments::AssetAuthArguments, + build_witness::{AssetAuthWitnessParams, build_asset_auth_witness}, +}; +use lwk_simplicity::wallet_abi::schema::{ + AssetVariant, BlinderVariant, FinalizerSpec, InputSchema, InputUnblinding, InternalKeySource, + LockVariant, OutputSchema, SimfArguments, SimfWitness, UTXOSource, serialize_arguments, + serialize_witness, +}; +use lwk_wollet::elements as el26; +use lwk_wollet::elements::{AssetId, OutPoint, Script}; +use simplicityhl::elements as el25; + +use crate::wallet_abi_common::{WalletAbiHarness, wallet_transfer_request}; + +#[derive(Clone, Debug)] +pub struct AssetAuthState { + pub arguments: AssetAuthArguments, + pub asset_id: AssetId, + pub amount_sat: u64, + pub last_outpoint: OutPoint, +} + +impl WalletAbiHarness { + pub async fn create_asset_auth( + &self, + asset_id: &AssetId, + amount_sat: u64, + arguments: AssetAuthArguments, + ) -> Result { + let asset_id_25 = asset_id + .to_string() + .parse::() + .context("failed to convert asset id from elements 0.26 to 0.25")?; + let tx = self + .process_request(wallet_transfer_request( + vec![InputSchema { + id: "locked-asset".into(), + utxo_source: UTXOSource::default(), + unblinding: InputUnblinding::Wallet, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }], + vec![OutputSchema { + id: "asset-auth".into(), + amount_sat, + lock: LockVariant::Finalizer { + finalizer: Box::new(FinalizerSpec::Simf { + source_simf: ASSET_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + arguments.build_asset_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_asset_auth_witness(&AssetAuthWitnessParams { + input_asset_index: 0, + output_asset_index: 0, + }), + runtime_arguments: vec![], + })?, + }), + }, + asset: AssetVariant::AssetId { + asset_id: *asset_id, + }, + blinder: BlinderVariant::Explicit, + }], + )) + .await?; + let auth_output = self.find_output(&tx, |tx_out| { + tx_out.asset.explicit() == Some(asset_id_25) + && tx_out.value.explicit() == Some(amount_sat) + })?; + if auth_output.asset_id_25()? != asset_id_25 { + bail!("asset-auth output asset id does not match requested asset id"); + } + + Ok(AssetAuthState { + arguments, + asset_id: auth_output.asset_id_26()?, + amount_sat: auth_output.value()?, + last_outpoint: auth_output.outpoint, + }) + } + + pub async fn unlock_asset_auth(&self, state: &mut AssetAuthState) -> Result<()> { + let mut outputs = vec![]; + let wallet_script = self.wallet_script_25().clone(); + let burn_script = Script::new_op_return(b"burn"); + let burn_script_bytes = burn_script.as_bytes().to_vec(); + let asset_id_25 = state + .asset_id + .to_string() + .parse::() + .context("failed to convert asset id from elements 0.26 to 0.25")?; + outputs.push(if state.arguments.with_asset_burn { + OutputSchema { + id: "auth-output".into(), + amount_sat: state.amount_sat, + lock: LockVariant::Script { + script: burn_script, + }, + asset: AssetVariant::AssetId { + asset_id: state.asset_id, + }, + blinder: BlinderVariant::Explicit, + } + } else { + OutputSchema { + id: "auth-output".into(), + amount_sat: state.amount_sat, + lock: LockVariant::Script { + script: self.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: state.asset_id, + }, + blinder: BlinderVariant::Explicit, + } + }); + + let tx = self + .process_request(wallet_transfer_request( + vec![InputSchema { + id: "asset-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.last_outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Simf { + source_simf: ASSET_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state.arguments.build_asset_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_asset_auth_witness(&AssetAuthWitnessParams { + input_asset_index: 0, + output_asset_index: 0, + }), + runtime_arguments: vec![], + })?, + }, + }], + outputs, + )) + .await?; + let auth_output = if state.arguments.with_asset_burn { + self.find_output(&tx, |tx_out| { + tx_out.script_pubkey.as_bytes() == burn_script_bytes.as_slice() + && tx_out.asset.explicit() == Some(asset_id_25) + && tx_out.value.explicit() == Some(state.amount_sat) + })? + } else { + self.find_output(&tx, |tx_out| { + tx_out.script_pubkey == wallet_script + && tx_out.asset.explicit() == Some(asset_id_25) + && tx_out.value.explicit() == Some(state.amount_sat) + })? + }; + + state.last_outpoint = auth_output.outpoint; + + Ok(()) + } +} diff --git a/crates/contracts/tests/support/wallet_abi_common.rs b/crates/contracts/tests/support/wallet_abi_common.rs new file mode 100644 index 0000000..70108f3 --- /dev/null +++ b/crates/contracts/tests/support/wallet_abi_common.rs @@ -0,0 +1,251 @@ +use std::net::TcpListener; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::LazyLock; +use std::sync::atomic::{AtomicU64, Ordering}; + +use anyhow::{Context, Result, anyhow, bail}; +use lwk_common::Network as RuntimeNetwork; +use lwk_simplicity::wallet_abi::schema::{ + InputSchema, OutputSchema, RuntimeParams, TX_CREATE_ABI_VERSION, TxCreateRequest, + generate_request_id, +}; +use lwk_simplicity::wallet_abi::test_utils::{ + TestSignerMeta, TestWalletMeta, build_runtime_parts_random, get_esplora_url, mine_blocks, +}; +use lwk_simplicity::wallet_abi::tx_resolution::runtime::Runtime; +use lwk_wollet::elements as el26; +use lwk_wollet::elements::hex::FromHex; +use simplicityhl::elements as el25; +use tokio::sync::{Mutex, OwnedMutexGuard}; +use tokio::time::sleep; + +static REGTEST_MUTEX: LazyLock>> = LazyLock::new(|| Arc::new(Mutex::new(()))); +static WALLET_DATA_COUNTER: AtomicU64 = AtomicU64::new(0); + +const REGTEST_PROCESS_LOCK_PORT: u16 = 45_193; +const REGTEST_PROCESS_LOCK_RETRIES: usize = 600; +const REGTEST_PROCESS_LOCK_DELAY: std::time::Duration = std::time::Duration::from_millis(500); + +#[derive(Clone, Debug)] +pub struct KnownUtxo { + pub outpoint: el26::OutPoint, + pub tx_out_26: el26::TxOut, + pub tx_out_25: el25::TxOut, +} + +impl KnownUtxo { + pub fn asset_id_26(&self) -> Result { + self.tx_out_26 + .asset + .explicit() + .context("expected explicit asset in elements 0.26 txout") + } + + pub fn asset_id_25(&self) -> Result { + self.tx_out_25 + .asset + .explicit() + .context("expected explicit asset in elements 0.25 txout") + } + + pub fn value(&self) -> Result { + self.tx_out_25 + .value + .explicit() + .context("expected explicit value") + } +} + +pub struct WalletAbiHarness { + _guard: OwnedMutexGuard<()>, + _process_lock: TcpListener, + pub(crate) signer_meta: TestSignerMeta, + pub(crate) wallet_meta: TestWalletMeta, + pub signer_address: el26::Address, + wallet_script_25: el25::Script, + wallet_script_26: el26::Script, +} + +impl WalletAbiHarness { + pub async fn new() -> Result { + let guard = REGTEST_MUTEX.clone().lock_owned().await; + let process_lock = acquire_regtest_process_lock().await?; + let esplora_url = get_esplora_url()?; + let (signer_meta, wallet_meta) = build_runtime_parts_random( + RuntimeNetwork::LocaltestLiquid, + &esplora_url, + wallet_data_root(), + ) + .await?; + let signer_address = signer_meta.signer_receive_address()?; + + Ok(Self { + _guard: guard, + _process_lock: process_lock, + signer_meta, + wallet_meta, + signer_address: signer_address.clone(), + wallet_script_25: script26_to25(&signer_address.script_pubkey())?, + wallet_script_26: signer_address.script_pubkey(), + }) + } + + pub const fn wallet_script_25(&self) -> &el25::Script { + &self.wallet_script_25 + } + + pub const fn wallet_script_26(&self) -> &el26::Script { + &self.wallet_script_26 + } + + pub async fn sync_wallet(&self) -> Result<()> { + self.wallet_meta.sync_wallet().await?; + Ok(()) + } + + pub async fn mine_and_sync(&self, blocks: usize) -> Result<()> { + mine_blocks(blocks)?; + self.sync_wallet().await + } + + pub async fn process_request(&self, request: TxCreateRequest) -> Result { + self.sync_wallet().await?; + + let response = Runtime::build(request, &self.signer_meta, &self.wallet_meta) + .process_request() + .await?; + let tx_info = response + .transaction + .context("wallet abi runtime did not return transaction info")?; + let tx26 = decode_tx_hex(&tx_info.tx_hex)?; + + if tx_info.txid != tx26.txid() { + bail!( + "wallet abi txid mismatch: response {} != decoded {}", + tx_info.txid, + tx26.txid() + ); + } + + self.mine_and_sync(1).await?; + + tx26_to25(&tx26) + } + + pub fn find_output( + &self, + tx: &el25::Transaction, + predicate: impl Fn(&el25::TxOut) -> bool, + ) -> Result { + let matches = tx + .output + .iter() + .enumerate() + .filter_map(|(vout, tx_out)| predicate(tx_out).then_some((vout, tx_out.clone()))) + .collect::>(); + if matches.len() != 1 { + bail!( + "expected exactly one matching output, found {}", + matches.len() + ); + } + let (vout, tx_out) = matches.into_iter().next().expect("checked len"); + let tx_out_26 = txout25_to26(&tx_out)?; + known_from_tx_output(tx, vout, tx_out_26) + } +} + +pub fn wallet_transfer_request( + inputs: Vec, + outputs: Vec, +) -> TxCreateRequest { + TxCreateRequest { + abi_version: TX_CREATE_ABI_VERSION.to_string(), + request_id: generate_request_id(), + network: RuntimeNetwork::LocaltestLiquid, + params: RuntimeParams { + inputs, + outputs, + fee_rate_sat_kvb: Some(0.1), + lock_time: None, + }, + broadcast: true, + } +} + +async fn acquire_regtest_process_lock() -> Result { + let lock_address = ("127.0.0.1", REGTEST_PROCESS_LOCK_PORT); + for attempt in 0..REGTEST_PROCESS_LOCK_RETRIES { + match TcpListener::bind(lock_address) { + Ok(listener) => return Ok(listener), + Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => { + if attempt + 1 == REGTEST_PROCESS_LOCK_RETRIES { + bail!( + "timed out waiting for Wallet ABI regtest lock on 127.0.0.1:{REGTEST_PROCESS_LOCK_PORT}" + ); + } + sleep(REGTEST_PROCESS_LOCK_DELAY).await; + } + Err(err) => { + return Err(err).context(format!( + "failed to bind Wallet ABI regtest lock on 127.0.0.1:{REGTEST_PROCESS_LOCK_PORT}" + )); + } + } + } + + unreachable!("lock acquisition either succeeds or returns an error") +} + +fn decode_tx_hex(tx_hex: &str) -> Result { + let bytes = Vec::::from_hex(tx_hex) + .map_err(|error| anyhow!("failed to decode transaction hex: {error}"))?; + Ok(el26::encode::deserialize(&bytes)?) +} + +pub fn known_from_tx_output( + tx: &el25::Transaction, + vout: usize, + tx_out_26: el26::TxOut, +) -> Result { + Ok(KnownUtxo { + outpoint: el26::OutPoint::new( + tx.txid().to_string().parse::()?, + u32::try_from(vout).context("vout does not fit in u32")?, + ), + tx_out_25: tx + .output + .get(vout) + .cloned() + .ok_or_else(|| anyhow!("transaction missing output {vout}"))?, + tx_out_26, + }) +} + +fn wallet_data_root() -> PathBuf { + let base = std::env::var_os("SIMPLICITY_CLI_WALLET_DATA_DIR").map_or_else( + || PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.cache/wallet"), + PathBuf::from, + ); + let unique_index = WALLET_DATA_COUNTER.fetch_add(1, Ordering::Relaxed); + base.join(format!( + "wallet-abi-lending-test-wallet-{}-{unique_index}", + std::process::id() + )) +} + +pub fn txout25_to26(value: &el25::TxOut) -> Result { + let bytes = el25::encode::serialize(value); + Ok(el26::encode::deserialize(&bytes)?) +} + +fn tx26_to25(value: &el26::Transaction) -> Result { + let bytes = el26::encode::serialize(value); + Ok(el25::encode::deserialize(&bytes)?) +} + +fn script26_to25(value: &el26::Script) -> Result { + let bytes = el26::encode::serialize(value); + Ok(el25::encode::deserialize(&bytes)?) +} diff --git a/crates/contracts/tests/support/wallet_abi_lending_support.rs b/crates/contracts/tests/support/wallet_abi_lending_support.rs new file mode 100644 index 0000000..3e8e3a0 --- /dev/null +++ b/crates/contracts/tests/support/wallet_abi_lending_support.rs @@ -0,0 +1,2099 @@ +use std::collections::HashMap; + +use anyhow::{Context, Result, anyhow}; +use lending_contracts::asset_auth::build_arguments::AssetAuthArguments; +use lending_contracts::asset_auth::build_witness::{ + AssetAuthWitnessParams, build_asset_auth_witness, +}; +use lending_contracts::lending::{ + LENDING_SOURCE, + build_witness::{LendingBranch, build_lending_witness}, +}; +use lending_contracts::pre_lock::PRE_LOCK_SOURCE; +use lending_contracts::script_auth::build_arguments::ScriptAuthArguments; +use lending_contracts::script_auth::build_witness::{ + ScriptAuthWitnessParams, build_script_auth_witness, +}; +use lwk_simplicity::scripts::{create_p2tr_address, load_program}; +use lwk_simplicity::wallet_abi::schema::{ + AssetVariant, BlinderVariant, FinalizerSpec, InputIssuance, InputIssuanceKind, InputSchema, + InputUnblinding, InternalKeySource, LockVariant, OutputSchema, RuntimeSimfWitness, + SimfArguments, SimfWitness, UTXOSource, serialize_arguments, serialize_witness, +}; +use lwk_simplicity::wallet_abi::test_utils::{RuntimeFundingAsset, fund_address}; +use lwk_wollet::elements as el26; +use lwk_wollet::secp256k1 as secp26; +use simplicityhl::elements as el25; +use simplicityhl::num::U256; +use simplicityhl::parse::ParseFromStr; +use simplicityhl::str::WitnessName; +use simplicityhl::types::TypeConstructible; +use simplicityhl::value::UIntValue; +use simplicityhl::value::ValueConstructible; +use simplicityhl::{Arguments, ResolvedType, Value, WitnessValues}; +use simplicityhl_core::hash_script; + +use crate::wallet_abi_common::{ + KnownUtxo, WalletAbiHarness, known_from_tx_output, txout25_to26, wallet_transfer_request, +}; + +const TOKENS_DECIMALS: u8 = 0; +const DEFAULT_COLLATERAL_AMOUNT: u64 = 25_000; +const DEFAULT_PRINCIPAL_AMOUNT: u64 = 10_000; +const DEFAULT_INTEREST_BPS: u16 = 500; +const FEE_FUNDING_AMOUNT: u64 = 1_000_000; +const INITIAL_LBTC_RESERVE_AMOUNT: u64 = 20_000_000; +const DEFAULT_EXPIRY_OFFSET: u32 = 64; +const LIQUIDATION_EXPIRY_OFFSET: u32 = 20; +const UTILITY_ISSUANCE_INPUT_VALUE: u64 = 100; +const UTILITY_ISSUANCE_ENTROPY: [u8; 32] = [7u8; 32]; +const MAX_BASIS_POINTS: u64 = 10_000; +const POWERS_OF_10: [u64; 16] = [ + 1, + 10, + 100, + 1_000, + 10_000, + 100_000, + 1_000_000, + 10_000_000, + 100_000_000, + 1_000_000_000, + 10_000_000_000, + 100_000_000_000, + 1_000_000_000_000, + 10_000_000_000_000, + 100_000_000_000_000, + 1_000_000_000_000_000, +]; + +#[derive(Clone, Debug)] +pub struct PreparedUtilityNfts { + pub issuance_utxos: [KnownUtxo; 4], +} + +#[derive(Clone, Debug)] +pub struct IssuedUtilityNfts { + pub borrower_nft: KnownUtxo, + pub lender_nft: KnownUtxo, + pub first_parameters_nft: KnownUtxo, + pub second_parameters_nft: KnownUtxo, +} + +#[derive(Clone, Debug)] +pub struct PreLockState { + pub arguments: PreLockContractArgs, + pub utility_script_auth_arguments: ScriptAuthArguments, + pub pre_lock: KnownUtxo, + pub first_parameters_nft: KnownUtxo, + pub second_parameters_nft: KnownUtxo, + pub borrower_nft: KnownUtxo, + pub lender_nft: KnownUtxo, +} + +#[derive(Clone, Debug)] +pub struct LendingState { + pub arguments: LendingContractArgs, + pub parameters_script_auth_arguments: ScriptAuthArguments, + pub lender_asset_auth_arguments: AssetAuthArguments, + pub lending: KnownUtxo, + pub first_parameters_nft: KnownUtxo, + pub second_parameters_nft: KnownUtxo, + pub borrower_nft: KnownUtxo, + pub lender_nft: KnownUtxo, + pub principal_borrowed: KnownUtxo, +} + +#[derive(Clone, Debug)] +pub struct RepaymentState { + pub asset_auth_arguments: AssetAuthArguments, + pub asset_auth: KnownUtxo, + pub collateral: KnownUtxo, + pub lender_nft: KnownUtxo, +} + +#[derive(Clone, Debug)] +pub struct ClaimState { + pub principal_claim: KnownUtxo, +} + +#[derive(Clone, Debug)] +pub struct LiquidationState { + pub collateral: KnownUtxo, +} + +#[derive(Debug, Clone, Copy)] +pub struct ProtocolTerms { + pub collateral_amount: u64, + pub principal_amount: u64, + pub loan_expiration_time: u32, + pub principal_interest_rate: u16, +} + +impl ProtocolTerms { + pub fn encode_parameters_nft_amounts(self, amounts_decimals: u8) -> Result<(u64, u64)> { + let first = encode_first_parameters_nft( + self.principal_interest_rate, + self.loan_expiration_time, + amounts_decimals, + amounts_decimals, + )?; + let second = encode_second_parameters_nft( + to_base_amount(self.collateral_amount, amounts_decimals), + to_base_amount(self.principal_amount, amounts_decimals), + )?; + Ok((first, second)) + } + + pub fn principal_with_interest(self) -> u64 { + calculate_principal_with_interest(self.principal_amount, self.principal_interest_rate) + } +} + +#[derive(Debug, Clone)] +pub struct PreLockContractArgs { + collateral_asset_id: [u8; 32], + principal_asset_id: [u8; 32], + borrower_nft_asset_id: [u8; 32], + lender_nft_asset_id: [u8; 32], + first_parameters_nft_asset_id: [u8; 32], + second_parameters_nft_asset_id: [u8; 32], + lending_cov_hash: [u8; 32], + parameters_nft_output_script_hash: [u8; 32], + borrower_nft_output_script_hash: [u8; 32], + principal_output_script_hash: [u8; 32], + borrower_pub_key: [u8; 32], + terms: ProtocolTerms, +} + +impl PreLockContractArgs { + #[allow(clippy::too_many_arguments)] + const fn new( + collateral_asset_id: [u8; 32], + principal_asset_id: [u8; 32], + borrower_nft_asset_id: [u8; 32], + lender_nft_asset_id: [u8; 32], + first_parameters_nft_asset_id: [u8; 32], + second_parameters_nft_asset_id: [u8; 32], + lending_cov_hash: [u8; 32], + parameters_nft_output_script_hash: [u8; 32], + borrower_nft_output_script_hash: [u8; 32], + principal_output_script_hash: [u8; 32], + borrower_pub_key: [u8; 32], + terms: ProtocolTerms, + ) -> Self { + Self { + collateral_asset_id, + principal_asset_id, + borrower_nft_asset_id, + lender_nft_asset_id, + first_parameters_nft_asset_id, + second_parameters_nft_asset_id, + lending_cov_hash, + parameters_nft_output_script_hash, + borrower_nft_output_script_hash, + principal_output_script_hash, + borrower_pub_key, + terms, + } + } + + fn build_simf_arguments(&self) -> Arguments { + Arguments::from(HashMap::from([ + argument_u256("COLLATERAL_ASSET_ID", self.collateral_asset_id), + argument_u256("PRINCIPAL_ASSET_ID", self.principal_asset_id), + argument_u256("BORROWER_NFT_ASSET_ID", self.borrower_nft_asset_id), + argument_u256("LENDER_NFT_ASSET_ID", self.lender_nft_asset_id), + argument_u256( + "FIRST_PARAMETERS_NFT_ASSET_ID", + self.first_parameters_nft_asset_id, + ), + argument_u256( + "SECOND_PARAMETERS_NFT_ASSET_ID", + self.second_parameters_nft_asset_id, + ), + argument_u256("LENDING_COV_HASH", self.lending_cov_hash), + argument_u256( + "PARAMETERS_NFT_OUTPUT_SCRIPT_HASH", + self.parameters_nft_output_script_hash, + ), + argument_u256( + "BORROWER_NFT_OUTPUT_SCRIPT_HASH", + self.borrower_nft_output_script_hash, + ), + argument_u256( + "PRINCIPAL_OUTPUT_SCRIPT_HASH", + self.principal_output_script_hash, + ), + argument_u256("BORROWER_PUB_KEY", self.borrower_pub_key), + argument_u64("COLLATERAL_AMOUNT", self.terms.collateral_amount), + argument_u64("PRINCIPAL_AMOUNT", self.terms.principal_amount), + argument_u32("LOAN_EXPIRATION_TIME", self.terms.loan_expiration_time), + argument_u16( + "PRINCIPAL_INTEREST_RATE", + self.terms.principal_interest_rate, + ), + ])) + } +} + +#[derive(Debug, Clone)] +pub struct LendingContractArgs { + collateral_asset_id: [u8; 32], + principal_asset_id: [u8; 32], + borrower_nft_asset_id: [u8; 32], + lender_nft_asset_id: [u8; 32], + first_parameters_nft_asset_id: [u8; 32], + second_parameters_nft_asset_id: [u8; 32], + lender_principal_cov_hash: [u8; 32], + terms: ProtocolTerms, +} + +impl LendingContractArgs { + #[allow(clippy::too_many_arguments)] + const fn new( + collateral_asset_id: [u8; 32], + principal_asset_id: [u8; 32], + borrower_nft_asset_id: [u8; 32], + lender_nft_asset_id: [u8; 32], + first_parameters_nft_asset_id: [u8; 32], + second_parameters_nft_asset_id: [u8; 32], + lender_principal_cov_hash: [u8; 32], + terms: ProtocolTerms, + ) -> Self { + Self { + collateral_asset_id, + principal_asset_id, + borrower_nft_asset_id, + lender_nft_asset_id, + first_parameters_nft_asset_id, + second_parameters_nft_asset_id, + lender_principal_cov_hash, + terms, + } + } + + fn build_simf_arguments(&self) -> Arguments { + Arguments::from(HashMap::from([ + argument_u256("COLLATERAL_ASSET_ID", self.collateral_asset_id), + argument_u256("PRINCIPAL_ASSET_ID", self.principal_asset_id), + argument_u256("BORROWER_NFT_ASSET_ID", self.borrower_nft_asset_id), + argument_u256("LENDER_NFT_ASSET_ID", self.lender_nft_asset_id), + argument_u256( + "FIRST_PARAMETERS_NFT_ASSET_ID", + self.first_parameters_nft_asset_id, + ), + argument_u256( + "SECOND_PARAMETERS_NFT_ASSET_ID", + self.second_parameters_nft_asset_id, + ), + argument_u256("LENDER_PRINCIPAL_COV_HASH", self.lender_principal_cov_hash), + argument_u64("COLLATERAL_AMOUNT", self.terms.collateral_amount), + argument_u64("PRINCIPAL_AMOUNT", self.terms.principal_amount), + argument_u32("LOAN_EXPIRATION_TIME", self.terms.loan_expiration_time), + argument_u16( + "PRINCIPAL_INTEREST_RATE", + self.terms.principal_interest_rate, + ), + ])) + } +} + +impl WalletAbiHarness { + fn signer_x_only_public_key_25(&self) -> Result { + xonly26_to25(&self.signer_meta.signer_x_only_public_key()?) + } + + fn signer_x_only_public_key_26(&self) -> Result { + self.signer_meta + .signer_x_only_public_key() + .map_err(Into::into) + } + + async fn fund_explicit_wallet_asset( + &self, + asset_id: el26::AssetId, + amount: u64, + output_id: &str, + ) -> Result { + let tx = self + .process_request(wallet_transfer_request( + vec![], + vec![OutputSchema { + id: output_id.into(), + amount_sat: amount, + lock: LockVariant::Script { + script: self.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { asset_id }, + blinder: BlinderVariant::Explicit, + }], + )) + .await?; + + self.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.wallet_script_25() + && tx_out.asset.explicit() == Some(asset_id26_to25(asset_id).expect("asset")) + && tx_out.value.explicit() == Some(amount) + }) + } + + async fn current_tip_height(&self) -> Result { + let response = minreq::get(format!( + "{}/blocks/tip/height", + lwk_simplicity::wallet_abi::test_utils::get_esplora_url()? + )) + .send() + .context("failed to query esplora tip height")?; + if response.status_code != 200 { + anyhow::bail!( + "unexpected esplora tip response status {}", + response.status_code + ); + } + + response + .as_str() + .context("failed to decode tip height body")? + .trim() + .parse::() + .context("failed to parse tip height") + } + + fn find_nth_output( + &self, + tx: &el25::Transaction, + predicate: impl Fn(&el25::TxOut) -> bool, + index: usize, + ) -> Result { + let matches = tx + .output + .iter() + .enumerate() + .filter_map(|(vout, tx_out)| predicate(tx_out).then_some((vout, tx_out.clone()))) + .collect::>(); + let (vout, tx_out) = matches + .get(index) + .cloned() + .ok_or_else(|| anyhow!("matching output index {index} out of bounds"))?; + let tx_out_26 = txout25_to26(&tx_out)?; + known_from_tx_output(tx, vout, tx_out_26) + } +} + +pub struct LendingScenario { + pub harness: WalletAbiHarness, + pub terms: ProtocolTerms, + pub policy_asset_id: el25::AssetId, + pub collateral_asset_id: el25::AssetId, + pub principal_asset_id: el25::AssetId, + pub principal_lend_utxo: KnownUtxo, + pub principal_repay_utxo: KnownUtxo, +} + +impl LendingScenario { + pub async fn new_default() -> Result { + Self::new_with_expiry_offset(DEFAULT_EXPIRY_OFFSET).await + } + + pub async fn new_for_liquidation() -> Result { + Self::new_with_expiry_offset(LIQUIDATION_EXPIRY_OFFSET).await + } + + async fn new_with_expiry_offset(expiry_offset: u32) -> Result { + let harness = WalletAbiHarness::new().await?; + let current_tip = harness.current_tip_height().await?; + let terms = ProtocolTerms { + collateral_amount: DEFAULT_COLLATERAL_AMOUNT, + principal_amount: DEFAULT_PRINCIPAL_AMOUNT, + loan_expiration_time: current_tip + .checked_add(expiry_offset) + .context("loan expiration height overflow")?, + principal_interest_rate: DEFAULT_INTEREST_BPS, + }; + + let lbtc_funding = fund_address( + &harness.signer_address, + RuntimeFundingAsset::Lbtc, + INITIAL_LBTC_RESERVE_AMOUNT, + )?; + let collateral_funding = fund_address( + &harness.signer_address, + RuntimeFundingAsset::IssuedAsset, + terms.collateral_amount, + )?; + let principal_funding = fund_address( + &harness.signer_address, + RuntimeFundingAsset::IssuedAsset, + terms + .principal_amount + .checked_add(terms.principal_with_interest()) + .context("principal funding amount overflow")?, + )?; + harness.mine_and_sync(1).await?; + + let policy_asset_id = asset_id26_to25(lbtc_funding.funded_asset_id)?; + let collateral_asset_id = asset_id26_to25(collateral_funding.funded_asset_id)?; + let principal_asset_id = asset_id26_to25(principal_funding.funded_asset_id)?; + let split_tx = harness + .process_request(wallet_transfer_request( + vec![InputSchema { + id: "principal-funding".into(), + utxo_source: UTXOSource::default(), + unblinding: InputUnblinding::Wallet, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }], + vec![ + OutputSchema { + id: "principal-lend".into(), + amount_sat: terms.principal_amount, + lock: LockVariant::Script { + script: harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: principal_funding.funded_asset_id, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "principal-repay".into(), + amount_sat: terms.principal_with_interest(), + lock: LockVariant::Script { + script: harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: principal_funding.funded_asset_id, + }, + blinder: BlinderVariant::Explicit, + }, + ], + )) + .await?; + let principal_lend_utxo = harness.find_output(&split_tx, |tx_out| { + tx_out.script_pubkey == *harness.wallet_script_25() + && tx_out.asset.explicit() == Some(principal_asset_id) + && tx_out.value.explicit() == Some(terms.principal_amount) + })?; + let principal_repay_utxo = harness.find_output(&split_tx, |tx_out| { + tx_out.script_pubkey == *harness.wallet_script_25() + && tx_out.asset.explicit() == Some(principal_asset_id) + && tx_out.value.explicit() == Some(terms.principal_with_interest()) + })?; + + Ok(Self { + harness, + terms, + policy_asset_id, + collateral_asset_id, + principal_asset_id, + principal_lend_utxo, + principal_repay_utxo, + }) + } + + async fn fund_explicit_policy_fee(&self, output_id: &str) -> Result { + self.harness + .fund_explicit_wallet_asset( + asset_id25_to26(self.policy_asset_id)?, + FEE_FUNDING_AMOUNT, + output_id, + ) + .await + } + + pub async fn prepare_utility_nfts_issuance(&self) -> Result { + let tx = self + .harness + .process_request(wallet_transfer_request( + vec![], + vec![ + OutputSchema { + id: "issuance-utxo-0".into(), + amount_sat: UTILITY_ISSUANCE_INPUT_VALUE, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.policy_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "issuance-utxo-1".into(), + amount_sat: UTILITY_ISSUANCE_INPUT_VALUE, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.policy_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "issuance-utxo-2".into(), + amount_sat: UTILITY_ISSUANCE_INPUT_VALUE, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.policy_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "issuance-utxo-3".into(), + amount_sat: UTILITY_ISSUANCE_INPUT_VALUE, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.policy_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + ], + )) + .await?; + let issuance_outputs = tx + .output + .iter() + .enumerate() + .filter_map(|(vout, tx_out)| { + (tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.asset.explicit() == Some(self.policy_asset_id) + && tx_out.value.explicit() == Some(UTILITY_ISSUANCE_INPUT_VALUE)) + .then_some((vout, tx_out.clone())) + }) + .map(|(vout, tx_out)| { + known_from_tx_output(&tx, vout, txout25_to26(&tx_out).expect("convert txout")) + }) + .collect::>>()?; + let issuance_utxos: [KnownUtxo; 4] = issuance_outputs + .try_into() + .map_err(|_| anyhow!("expected four issuance outputs"))?; + + Ok(PreparedUtilityNfts { issuance_utxos }) + } + + pub async fn issue_utility_nfts( + &self, + prepared: &PreparedUtilityNfts, + ) -> Result { + let (first_parameters_amount, second_parameters_amount) = + self.terms.encode_parameters_nft_amounts(TOKENS_DECIMALS)?; + let issuance_asset = asset_id25_to26(self.policy_asset_id)?; + let fee_utxo = self.fund_explicit_policy_fee("issue-fee").await?; + let tx = self + .harness + .process_request(wallet_transfer_request( + vec![ + InputSchema { + id: "borrower-issuance".into(), + utxo_source: UTXOSource::Provided { + outpoint: prepared.issuance_utxos[0].outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::MAX, + issuance: Some(InputIssuance { + kind: InputIssuanceKind::New, + asset_amount_sat: 1, + token_amount_sat: 0, + entropy: UTILITY_ISSUANCE_ENTROPY, + }), + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "lender-issuance".into(), + utxo_source: UTXOSource::Provided { + outpoint: prepared.issuance_utxos[1].outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::MAX, + issuance: Some(InputIssuance { + kind: InputIssuanceKind::New, + asset_amount_sat: 1, + token_amount_sat: 0, + entropy: UTILITY_ISSUANCE_ENTROPY, + }), + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "first-parameters-issuance".into(), + utxo_source: UTXOSource::Provided { + outpoint: prepared.issuance_utxos[2].outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::MAX, + issuance: Some(InputIssuance { + kind: InputIssuanceKind::New, + asset_amount_sat: first_parameters_amount, + token_amount_sat: 0, + entropy: UTILITY_ISSUANCE_ENTROPY, + }), + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "second-parameters-issuance".into(), + utxo_source: UTXOSource::Provided { + outpoint: prepared.issuance_utxos[3].outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::MAX, + issuance: Some(InputIssuance { + kind: InputIssuanceKind::New, + asset_amount_sat: second_parameters_amount, + token_amount_sat: 0, + entropy: UTILITY_ISSUANCE_ENTROPY, + }), + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "issue-fee".into(), + utxo_source: UTXOSource::Provided { + outpoint: fee_utxo.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + ], + vec![ + OutputSchema { + id: "borrower-nft".into(), + amount_sat: 1, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::NewIssuanceAsset { input_index: 0 }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "lender-nft".into(), + amount_sat: 1, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::NewIssuanceAsset { input_index: 1 }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "first-parameters-nft".into(), + amount_sat: first_parameters_amount, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::NewIssuanceAsset { input_index: 2 }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "second-parameters-nft".into(), + amount_sat: second_parameters_amount, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::NewIssuanceAsset { input_index: 3 }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "return-issuance-0".into(), + amount_sat: UTILITY_ISSUANCE_INPUT_VALUE, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: issuance_asset, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "return-issuance-1".into(), + amount_sat: UTILITY_ISSUANCE_INPUT_VALUE, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: issuance_asset, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "return-issuance-2".into(), + amount_sat: UTILITY_ISSUANCE_INPUT_VALUE, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: issuance_asset, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "return-issuance-3".into(), + amount_sat: UTILITY_ISSUANCE_INPUT_VALUE, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: issuance_asset, + }, + blinder: BlinderVariant::Explicit, + }, + ], + )) + .await?; + + let borrower_nft = self.harness.find_nth_output( + &tx, + |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.value.explicit() == Some(1) + && tx_out.asset.explicit() != Some(self.policy_asset_id) + }, + 0, + )?; + let lender_nft = self.harness.find_nth_output( + &tx, + |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.value.explicit() == Some(1) + && tx_out.asset.explicit() != Some(self.policy_asset_id) + }, + 1, + )?; + let first_parameters_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.value.explicit() == Some(first_parameters_amount) + && tx_out.asset.explicit() != Some(self.policy_asset_id) + })?; + let second_parameters_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.value.explicit() == Some(second_parameters_amount) + && tx_out.asset.explicit() != Some(self.policy_asset_id) + })?; + + Ok(IssuedUtilityNfts { + borrower_nft, + lender_nft, + first_parameters_nft, + second_parameters_nft, + }) + } + + pub async fn create_pre_lock(&self, nfts: &IssuedUtilityNfts) -> Result { + let lender_asset_auth_arguments = + AssetAuthArguments::new(nfts.lender_nft.asset_id_25()?.into_inner().0, 1, true); + let lender_asset_auth_address = create_p2tr_address( + load_program( + lending_contracts::asset_auth::ASSET_AUTH_SOURCE, + lender_asset_auth_arguments.build_asset_auth_arguments(), + )? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ); + let lending_arguments = LendingContractArgs::new( + self.collateral_asset_id.into_inner().0, + self.principal_asset_id.into_inner().0, + nfts.borrower_nft.asset_id_25()?.into_inner().0, + nfts.lender_nft.asset_id_25()?.into_inner().0, + nfts.first_parameters_nft.asset_id_25()?.into_inner().0, + nfts.second_parameters_nft.asset_id_25()?.into_inner().0, + hash_script(&lender_asset_auth_address.script_pubkey()), + self.terms, + ); + let lending_address = create_p2tr_address( + load_program(LENDING_SOURCE, lending_arguments.build_simf_arguments())? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ); + let parameters_script_auth_arguments = + ScriptAuthArguments::new(hash_script(&lending_address.script_pubkey())); + let wallet_script_hash = hash_script(self.harness.wallet_script_25()); + let parameters_script_auth_address = create_p2tr_address( + load_program( + lending_contracts::script_auth::SCRIPT_AUTH_SOURCE, + parameters_script_auth_arguments.build_script_auth_arguments(), + )? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ); + let arguments = PreLockContractArgs::new( + self.collateral_asset_id.into_inner().0, + self.principal_asset_id.into_inner().0, + nfts.borrower_nft.asset_id_25()?.into_inner().0, + nfts.lender_nft.asset_id_25()?.into_inner().0, + nfts.first_parameters_nft.asset_id_25()?.into_inner().0, + nfts.second_parameters_nft.asset_id_25()?.into_inner().0, + hash_script(&lending_address.script_pubkey()), + hash_script(¶meters_script_auth_address.script_pubkey()), + wallet_script_hash, + wallet_script_hash, + self.harness.signer_x_only_public_key_25()?.serialize(), + self.terms, + ); + let pre_lock_address = create_p2tr_address( + load_program(PRE_LOCK_SOURCE, arguments.build_simf_arguments())? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ); + let utility_script_auth_arguments = + ScriptAuthArguments::new(hash_script(&pre_lock_address.script_pubkey())); + let utility_script_auth_address = create_p2tr_address( + load_program( + lending_contracts::script_auth::SCRIPT_AUTH_SOURCE, + utility_script_auth_arguments.build_script_auth_arguments(), + )? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ); + + let mut op_return_data = [0u8; 64]; + op_return_data[..32] + .copy_from_slice(&self.harness.signer_x_only_public_key_25()?.serialize()); + op_return_data[32..].copy_from_slice(&self.principal_asset_id.into_inner().0); + let metadata_script = { + let bytes = el25::encode::serialize(&el25::Script::new_op_return(&op_return_data)); + el26::encode::deserialize(&bytes)? + }; + let fee_utxo = self.fund_explicit_policy_fee("pre-lock-fee").await?; + let pre_lock_creation_finalizer = FinalizerSpec::Simf { + source_simf: PRE_LOCK_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new(arguments.build_simf_arguments()))?, + witness: serialize_witness(&SimfWitness { + resolved: runtime_pre_lock_lending_creation_witness(), + runtime_arguments: vec![], + })?, + }; + let utility_script_auth_output_finalizer = FinalizerSpec::Simf { + source_simf: lending_contracts::script_auth::SCRIPT_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + utility_script_auth_arguments.build_script_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_script_auth_witness(&ScriptAuthWitnessParams { + input_script_index: 0, + }), + runtime_arguments: vec![], + })?, + }; + + let tx = self + .harness + .process_request(wallet_transfer_request( + vec![ + InputSchema { + id: "collateral".into(), + utxo_source: UTXOSource::default(), + unblinding: InputUnblinding::Wallet, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "first-parameters".into(), + utxo_source: UTXOSource::Provided { + outpoint: nfts.first_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "second-parameters".into(), + utxo_source: UTXOSource::Provided { + outpoint: nfts.second_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "borrower-nft".into(), + utxo_source: UTXOSource::Provided { + outpoint: nfts.borrower_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "lender-nft".into(), + utxo_source: UTXOSource::Provided { + outpoint: nfts.lender_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "pre-lock-fee".into(), + utxo_source: UTXOSource::Provided { + outpoint: fee_utxo.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + ], + vec![ + OutputSchema { + id: "pre-lock".into(), + amount_sat: self.terms.collateral_amount, + lock: LockVariant::Finalizer { + finalizer: Box::new(pre_lock_creation_finalizer), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.collateral_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "first-parameters-script-auth".into(), + amount_sat: nfts.first_parameters_nft.value()?, + lock: LockVariant::Finalizer { + finalizer: Box::new(utility_script_auth_output_finalizer.clone()), + }, + asset: AssetVariant::AssetId { + asset_id: nfts.first_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "second-parameters-script-auth".into(), + amount_sat: nfts.second_parameters_nft.value()?, + lock: LockVariant::Finalizer { + finalizer: Box::new(utility_script_auth_output_finalizer.clone()), + }, + asset: AssetVariant::AssetId { + asset_id: nfts.second_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "borrower-script-auth".into(), + amount_sat: 1, + lock: LockVariant::Finalizer { + finalizer: Box::new(utility_script_auth_output_finalizer.clone()), + }, + asset: AssetVariant::AssetId { + asset_id: nfts.borrower_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "lender-script-auth".into(), + amount_sat: 1, + lock: LockVariant::Finalizer { + finalizer: Box::new(utility_script_auth_output_finalizer), + }, + asset: AssetVariant::AssetId { + asset_id: nfts.lender_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "pre-lock-metadata".into(), + amount_sat: 0, + lock: LockVariant::Script { + script: metadata_script, + }, + asset: AssetVariant::AssetId { + asset_id: el26::AssetId::default(), + }, + blinder: BlinderVariant::Explicit, + }, + ], + )) + .await?; + let pre_lock = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == pre_lock_address.script_pubkey() + && tx_out.asset.explicit() == Some(self.collateral_asset_id) + && tx_out.value.explicit() == Some(self.terms.collateral_amount) + })?; + let first_parameters_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == utility_script_auth_address.script_pubkey() + && tx_out.asset.explicit() + == Some(nfts.first_parameters_nft.asset_id_25().expect("asset")) + && tx_out.value.explicit() + == Some(nfts.first_parameters_nft.value().expect("value")) + })?; + let second_parameters_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == utility_script_auth_address.script_pubkey() + && tx_out.asset.explicit() + == Some(nfts.second_parameters_nft.asset_id_25().expect("asset")) + && tx_out.value.explicit() + == Some(nfts.second_parameters_nft.value().expect("value")) + })?; + let borrower_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == utility_script_auth_address.script_pubkey() + && tx_out.asset.explicit() == Some(nfts.borrower_nft.asset_id_25().expect("asset")) + && tx_out.value.explicit() == Some(1) + })?; + let lender_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == utility_script_auth_address.script_pubkey() + && tx_out.asset.explicit() == Some(nfts.lender_nft.asset_id_25().expect("asset")) + && tx_out.value.explicit() == Some(1) + })?; + + Ok(PreLockState { + arguments, + utility_script_auth_arguments, + pre_lock, + first_parameters_nft, + second_parameters_nft, + borrower_nft, + lender_nft, + }) + } + + pub async fn cancel_pre_lock(&self, state: &PreLockState) -> Result { + let fee_utxo = self.fund_explicit_policy_fee("cancel-fee").await?; + let pre_lock_cancellation_finalizer = FinalizerSpec::Simf { + source_simf: PRE_LOCK_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state.arguments.build_simf_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: runtime_pre_lock_cancellation_path(), + runtime_arguments: vec![RuntimeSimfWitness::SigHashAll { + name: "CANCELLATION_SIGNATURE".to_string(), + public_key: self.harness.signer_x_only_public_key_26()?, + }], + })?, + }; + let utility_script_auth_finalizer = FinalizerSpec::Simf { + source_simf: lending_contracts::script_auth::SCRIPT_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state + .utility_script_auth_arguments + .build_script_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_script_auth_witness(&lending_script_witness()?), + runtime_arguments: vec![], + })?, + }; + let tx = self + .harness + .process_request(wallet_transfer_request( + vec![ + InputSchema { + id: "pre-lock".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.pre_lock.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: pre_lock_cancellation_finalizer, + }, + InputSchema { + id: "first-parameters-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.first_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: utility_script_auth_finalizer.clone(), + }, + InputSchema { + id: "second-parameters-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.second_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: utility_script_auth_finalizer.clone(), + }, + InputSchema { + id: "borrower-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.borrower_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: utility_script_auth_finalizer.clone(), + }, + InputSchema { + id: "lender-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.lender_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: utility_script_auth_finalizer, + }, + InputSchema { + id: "cancel-fee".into(), + utxo_source: UTXOSource::Provided { + outpoint: fee_utxo.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + ], + vec![ + OutputSchema { + id: "collateral-return".into(), + amount_sat: self.terms.collateral_amount, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.collateral_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "first-parameters-burn".into(), + amount_sat: state.first_parameters_nft.value()?, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.first_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "second-parameters-burn".into(), + amount_sat: state.second_parameters_nft.value()?, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.second_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "borrower-burn".into(), + amount_sat: state.borrower_nft.value()?, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.borrower_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "lender-burn".into(), + amount_sat: state.lender_nft.value()?, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.lender_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + ], + )) + .await?; + self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.asset.explicit() == Some(self.collateral_asset_id) + && tx_out.value.explicit() == Some(self.terms.collateral_amount) + }) + } + + pub async fn create_lending(&self, state: &PreLockState) -> Result { + let lender_asset_auth_arguments = + AssetAuthArguments::new(state.lender_nft.asset_id_25()?.into_inner().0, 1, true); + let lender_asset_auth_address = create_p2tr_address( + load_program( + lending_contracts::asset_auth::ASSET_AUTH_SOURCE, + lender_asset_auth_arguments.build_asset_auth_arguments(), + )? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ); + let arguments = LendingContractArgs::new( + self.collateral_asset_id.into_inner().0, + self.principal_asset_id.into_inner().0, + state.borrower_nft.asset_id_25()?.into_inner().0, + state.lender_nft.asset_id_25()?.into_inner().0, + state.first_parameters_nft.asset_id_25()?.into_inner().0, + state.second_parameters_nft.asset_id_25()?.into_inner().0, + hash_script(&lender_asset_auth_address.script_pubkey()), + self.terms, + ); + let lending_address = create_p2tr_address( + load_program(LENDING_SOURCE, arguments.build_simf_arguments())? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ); + let parameters_script_auth_arguments = + ScriptAuthArguments::new(hash_script(&lending_address.script_pubkey())); + let parameters_script_auth_address = create_p2tr_address( + load_program( + lending_contracts::script_auth::SCRIPT_AUTH_SOURCE, + parameters_script_auth_arguments.build_script_auth_arguments(), + )? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ); + let fee_utxo = self.fund_explicit_policy_fee("lending-fee").await?; + let pre_lock_creation_finalizer = FinalizerSpec::Simf { + source_simf: PRE_LOCK_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state.arguments.build_simf_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: runtime_pre_lock_lending_creation_witness(), + runtime_arguments: vec![], + })?, + }; + let utility_script_auth_finalizer = FinalizerSpec::Simf { + source_simf: lending_contracts::script_auth::SCRIPT_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state + .utility_script_auth_arguments + .build_script_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_script_auth_witness(&lending_script_witness()?), + runtime_arguments: vec![], + })?, + }; + let lending_output_finalizer = FinalizerSpec::Simf { + source_simf: LENDING_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new(arguments.build_simf_arguments()))?, + witness: serialize_witness(&SimfWitness { + resolved: build_lending_witness(LendingBranch::LoanRepayment), + runtime_arguments: vec![], + })?, + }; + let parameters_script_auth_output_finalizer = FinalizerSpec::Simf { + source_simf: lending_contracts::script_auth::SCRIPT_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + parameters_script_auth_arguments.build_script_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_script_auth_witness(&ScriptAuthWitnessParams { + input_script_index: 0, + }), + runtime_arguments: vec![], + })?, + }; + + let tx = self + .harness + .process_request(wallet_transfer_request( + vec![ + InputSchema { + id: "pre-lock".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.pre_lock.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: pre_lock_creation_finalizer, + }, + InputSchema { + id: "first-parameters-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.first_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: utility_script_auth_finalizer.clone(), + }, + InputSchema { + id: "second-parameters-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.second_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: utility_script_auth_finalizer.clone(), + }, + InputSchema { + id: "borrower-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.borrower_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: utility_script_auth_finalizer.clone(), + }, + InputSchema { + id: "lender-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.lender_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: utility_script_auth_finalizer, + }, + InputSchema { + id: "principal-lend".into(), + utxo_source: UTXOSource::Provided { + outpoint: self.principal_lend_utxo.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "lending-fee".into(), + utxo_source: UTXOSource::Provided { + outpoint: fee_utxo.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + ], + vec![ + OutputSchema { + id: "lending".into(), + amount_sat: self.terms.collateral_amount, + lock: LockVariant::Finalizer { + finalizer: Box::new(lending_output_finalizer), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.collateral_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "principal-to-wallet".into(), + amount_sat: self.terms.principal_amount, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.principal_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "first-parameters-script-auth".into(), + amount_sat: state.first_parameters_nft.value()?, + lock: LockVariant::Finalizer { + finalizer: Box::new(parameters_script_auth_output_finalizer.clone()), + }, + asset: AssetVariant::AssetId { + asset_id: state.first_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "second-parameters-script-auth".into(), + amount_sat: state.second_parameters_nft.value()?, + lock: LockVariant::Finalizer { + finalizer: Box::new(parameters_script_auth_output_finalizer), + }, + asset: AssetVariant::AssetId { + asset_id: state.second_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "borrower-nft-to-wallet".into(), + amount_sat: 1, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: state.borrower_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "lender-nft-to-wallet".into(), + amount_sat: 1, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: state.lender_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + ], + )) + .await?; + let lending = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == lending_address.script_pubkey() + && tx_out.asset.explicit() == Some(self.collateral_asset_id) + && tx_out.value.explicit() == Some(self.terms.collateral_amount) + })?; + let first_parameters_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == parameters_script_auth_address.script_pubkey() + && tx_out.asset.explicit() + == Some(state.first_parameters_nft.asset_id_25().expect("asset")) + && tx_out.value.explicit() + == Some(state.first_parameters_nft.value().expect("value")) + })?; + let second_parameters_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == parameters_script_auth_address.script_pubkey() + && tx_out.asset.explicit() + == Some(state.second_parameters_nft.asset_id_25().expect("asset")) + && tx_out.value.explicit() + == Some(state.second_parameters_nft.value().expect("value")) + })?; + let borrower_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.asset.explicit() == Some(state.borrower_nft.asset_id_25().expect("asset")) + && tx_out.value.explicit() == Some(1) + })?; + let lender_nft = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.asset.explicit() == Some(state.lender_nft.asset_id_25().expect("asset")) + && tx_out.value.explicit() == Some(1) + })?; + let principal_borrowed = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.asset.explicit() == Some(self.principal_asset_id) + && tx_out.value.explicit() == Some(self.terms.principal_amount) + })?; + + Ok(LendingState { + arguments, + parameters_script_auth_arguments, + lender_asset_auth_arguments, + lending, + first_parameters_nft, + second_parameters_nft, + borrower_nft, + lender_nft, + principal_borrowed, + }) + } + + pub async fn repay_loan(&self, state: &LendingState) -> Result { + let fee_utxo = self.fund_explicit_policy_fee("repayment-fee").await?; + let lending_repayment_finalizer = FinalizerSpec::Simf { + source_simf: LENDING_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state.arguments.build_simf_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_lending_witness(LendingBranch::LoanRepayment), + runtime_arguments: vec![], + })?, + }; + let parameters_script_auth_finalizer = FinalizerSpec::Simf { + source_simf: lending_contracts::script_auth::SCRIPT_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state + .parameters_script_auth_arguments + .build_script_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_script_auth_witness(&lending_script_witness()?), + runtime_arguments: vec![], + })?, + }; + let lender_asset_auth_output_finalizer = FinalizerSpec::Simf { + source_simf: lending_contracts::asset_auth::ASSET_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state + .lender_asset_auth_arguments + .build_asset_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_asset_auth_witness(&AssetAuthWitnessParams { + input_asset_index: 0, + output_asset_index: 0, + }), + runtime_arguments: vec![], + })?, + }; + let lender_asset_auth_address = create_p2tr_address( + load_program( + lending_contracts::asset_auth::ASSET_AUTH_SOURCE, + state + .lender_asset_auth_arguments + .build_asset_auth_arguments(), + )? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ); + let tx = self + .harness + .process_request(wallet_transfer_request( + vec![ + InputSchema { + id: "lending".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.lending.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: lending_repayment_finalizer, + }, + InputSchema { + id: "first-parameters-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.first_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: parameters_script_auth_finalizer.clone(), + }, + InputSchema { + id: "second-parameters-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.second_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: parameters_script_auth_finalizer, + }, + InputSchema { + id: "borrower-nft".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.borrower_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "principal-repay".into(), + utxo_source: UTXOSource::Provided { + outpoint: self.principal_repay_utxo.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "repayment-fee".into(), + utxo_source: UTXOSource::Provided { + outpoint: fee_utxo.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + ], + vec![ + OutputSchema { + id: "collateral-return".into(), + amount_sat: self.terms.collateral_amount, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.collateral_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "lender-asset-auth".into(), + amount_sat: self.terms.principal_with_interest(), + lock: LockVariant::Finalizer { + finalizer: Box::new(lender_asset_auth_output_finalizer), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.principal_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "first-parameters-burn".into(), + amount_sat: state.first_parameters_nft.value()?, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.first_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "second-parameters-burn".into(), + amount_sat: state.second_parameters_nft.value()?, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.second_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "borrower-burn".into(), + amount_sat: state.borrower_nft.value()?, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.borrower_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + ], + )) + .await?; + let asset_auth = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == lender_asset_auth_address.script_pubkey() + && tx_out.asset.explicit() == Some(self.principal_asset_id) + && tx_out.value.explicit() == Some(self.terms.principal_with_interest()) + })?; + let collateral = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.asset.explicit() == Some(self.collateral_asset_id) + && tx_out.value.explicit() == Some(self.terms.collateral_amount) + })?; + + Ok(RepaymentState { + asset_auth_arguments: state.lender_asset_auth_arguments.clone(), + asset_auth, + collateral, + lender_nft: state.lender_nft.clone(), + }) + } + + pub async fn claim_repaid_principal(&self, state: &RepaymentState) -> Result { + let fee_utxo = self.fund_explicit_policy_fee("claim-fee").await?; + let asset_auth_claim_finalizer = FinalizerSpec::Simf { + source_simf: lending_contracts::asset_auth::ASSET_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state.asset_auth_arguments.build_asset_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_asset_auth_witness(&lending_asset_auth_claim_witness()), + runtime_arguments: vec![], + })?, + }; + let tx = self + .harness + .process_request(wallet_transfer_request( + vec![ + InputSchema { + id: "asset-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.asset_auth.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: asset_auth_claim_finalizer, + }, + InputSchema { + id: "lender-nft".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.lender_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "claim-fee".into(), + utxo_source: UTXOSource::Provided { + outpoint: fee_utxo.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + ], + vec![ + OutputSchema { + id: "principal-claim".into(), + amount_sat: self.terms.principal_with_interest(), + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.principal_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "lender-burn".into(), + amount_sat: 1, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.lender_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + ], + )) + .await?; + let principal_claim = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.asset.explicit() == Some(self.principal_asset_id) + && tx_out.value.explicit() == Some(self.terms.principal_with_interest()) + })?; + + Ok(ClaimState { principal_claim }) + } + + pub async fn liquidate_loan(&self, state: &LendingState) -> Result { + let current_tip = self.harness.current_tip_height().await?; + let blocks_to_mine = self + .terms + .loan_expiration_time + .saturating_sub(current_tip) + .saturating_add(1); + if blocks_to_mine > 0 { + self.harness.mine_and_sync(blocks_to_mine as usize).await?; + } + let fee_utxo = self.fund_explicit_policy_fee("liquidation-fee").await?; + let lending_liquidation_finalizer = FinalizerSpec::Simf { + source_simf: LENDING_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state.arguments.build_simf_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_lending_witness(LendingBranch::LoanLiquidation), + runtime_arguments: vec![], + })?, + }; + let parameters_script_auth_finalizer = FinalizerSpec::Simf { + source_simf: lending_contracts::script_auth::SCRIPT_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state + .parameters_script_auth_arguments + .build_script_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_script_auth_witness(&lending_script_witness()?), + runtime_arguments: vec![], + })?, + }; + + let tx = self + .harness + .process_request({ + let mut request = wallet_transfer_request( + vec![ + InputSchema { + id: "lending".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.lending.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: lending_liquidation_finalizer, + }, + InputSchema { + id: "first-parameters-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.first_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: parameters_script_auth_finalizer.clone(), + }, + InputSchema { + id: "second-parameters-script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.second_parameters_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: parameters_script_auth_finalizer, + }, + InputSchema { + id: "lender-nft".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.lender_nft.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + InputSchema { + id: "liquidation-fee".into(), + utxo_source: UTXOSource::Provided { + outpoint: fee_utxo.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + ], + vec![ + OutputSchema { + id: "collateral-return".into(), + amount_sat: self.terms.collateral_amount, + lock: LockVariant::Script { + script: self.harness.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: asset_id25_to26(self.collateral_asset_id)?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "first-parameters-burn".into(), + amount_sat: state.first_parameters_nft.value()?, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.first_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "second-parameters-burn".into(), + amount_sat: state.second_parameters_nft.value()?, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.second_parameters_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "lender-burn".into(), + amount_sat: 1, + lock: LockVariant::Script { + script: el26::Script::new_op_return(b"burn"), + }, + asset: AssetVariant::AssetId { + asset_id: state.lender_nft.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + ], + ); + request.params.lock_time = Some(el26::LockTime::from_height( + self.terms.loan_expiration_time, + )?); + request + }) + .await?; + let collateral = self.harness.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.harness.wallet_script_25() + && tx_out.asset.explicit() == Some(self.collateral_asset_id) + && tx_out.value.explicit() == Some(self.terms.collateral_amount) + })?; + + Ok(LiquidationState { collateral }) + } +} + +fn runtime_pre_lock_cancellation_path() -> WitnessValues { + let path_type = ResolvedType::either( + ResolvedType::parse_from_str("()").expect("valid type"), + ResolvedType::parse_from_str("()").expect("valid type"), + ); + WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("PATH"), + Value::parse_from_str("Right(())", &path_type).expect("valid witness"), + )])) +} + +fn runtime_pre_lock_lending_creation_witness() -> WitnessValues { + let path_type = ResolvedType::either( + ResolvedType::parse_from_str("()").expect("valid type"), + ResolvedType::parse_from_str("()").expect("valid type"), + ); + WitnessValues::from(HashMap::from([ + ( + WitnessName::from_str_unchecked("PATH"), + Value::parse_from_str("Left(())", &path_type).expect("valid witness"), + ), + ( + WitnessName::from_str_unchecked("CANCELLATION_SIGNATURE"), + Value::byte_array([0u8; 64]), + ), + ])) +} + +const fn lending_script_witness() +-> Result { + Ok( + lending_contracts::script_auth::build_witness::ScriptAuthWitnessParams { + input_script_index: 0, + }, + ) +} + +const fn lending_asset_auth_claim_witness() +-> lending_contracts::asset_auth::build_witness::AssetAuthWitnessParams { + lending_contracts::asset_auth::build_witness::AssetAuthWitnessParams { + input_asset_index: 1, + output_asset_index: 1, + } +} + +fn argument_u256(name: &str, bytes: [u8; 32]) -> (WitnessName, simplicityhl::Value) { + ( + WitnessName::from_str_unchecked(name), + simplicityhl::Value::from(UIntValue::U256(U256::from_byte_array(bytes))), + ) +} + +fn argument_u64(name: &str, value: u64) -> (WitnessName, simplicityhl::Value) { + ( + WitnessName::from_str_unchecked(name), + simplicityhl::Value::from(UIntValue::U64(value)), + ) +} + +fn argument_u32(name: &str, value: u32) -> (WitnessName, simplicityhl::Value) { + ( + WitnessName::from_str_unchecked(name), + simplicityhl::Value::from(UIntValue::U32(value)), + ) +} + +fn argument_u16(name: &str, value: u16) -> (WitnessName, simplicityhl::Value) { + ( + WitnessName::from_str_unchecked(name), + simplicityhl::Value::from(UIntValue::U16(value)), + ) +} + +fn calculate_principal_with_interest(principal_amount: u64, interest_rate: u16) -> u64 { + let interest = + (u128::from(principal_amount) * u128::from(interest_rate)) / u128::from(MAX_BASIS_POINTS); + principal_amount + .checked_add(u64::try_from(interest).expect("interest fits in u64")) + .expect("principal with interest overflow") +} + +fn encode_first_parameters_nft( + interest_rate: u16, + loan_expiration_time: u32, + collateral_dec: u8, + principal_dec: u8, +) -> Result { + if loan_expiration_time >= (1 << 27) { + return Err(anyhow!( + "parameter value out of bounds: loan expiration time" + )); + } + if collateral_dec >= (1 << 4) { + return Err(anyhow!( + "parameter value out of bounds: collateral decimals" + )); + } + if principal_dec >= (1 << 4) { + return Err(anyhow!("parameter value out of bounds: principal decimals")); + } + + Ok(u64::from(interest_rate) + | (u64::from(loan_expiration_time) << 16) + | (u64::from(collateral_dec) << 43) + | (u64::from(principal_dec) << 47)) +} + +fn encode_second_parameters_nft( + collateral_base_amount: u32, + principal_base_amount: u32, +) -> Result { + if collateral_base_amount >= (1 << 25) { + return Err(anyhow!( + "parameter value out of bounds: collateral base amount" + )); + } + if principal_base_amount >= (1 << 25) { + return Err(anyhow!( + "parameter value out of bounds: principal base amount" + )); + } + + Ok(u64::from(collateral_base_amount) | (u64::from(principal_base_amount) << 25)) +} + +fn to_base_amount(amount: u64, decimals_mantissa: u8) -> u32 { + let divisor = *POWERS_OF_10 + .get(decimals_mantissa as usize) + .expect("decimals mantissa must be between 0 and 15"); + amount + .checked_div(divisor) + .expect("division by zero") + .try_into() + .expect("base amount greater than u32") +} + +fn asset_id25_to26(value: el25::AssetId) -> Result { + value + .to_string() + .parse::() + .context("failed to convert asset id from elements 0.25 to 0.26") +} + +fn asset_id26_to25(value: el26::AssetId) -> Result { + value + .to_string() + .parse::() + .context("failed to convert asset id from elements 0.26 to 0.25") +} + +fn xonly26_to25(value: &secp26::XOnlyPublicKey) -> Result { + el25::schnorr::XOnlyPublicKey::from_slice(&value.serialize()) + .context("failed to convert x-only public key") +} diff --git a/crates/contracts/tests/support/wallet_abi_script_auth_support.rs b/crates/contracts/tests/support/wallet_abi_script_auth_support.rs new file mode 100644 index 0000000..acf1f79 --- /dev/null +++ b/crates/contracts/tests/support/wallet_abi_script_auth_support.rs @@ -0,0 +1,179 @@ +use anyhow::{Context, Result}; +use lending_contracts::script_auth::{ + SCRIPT_AUTH_SOURCE, + build_arguments::ScriptAuthArguments, + build_witness::{ScriptAuthWitnessParams, build_script_auth_witness}, +}; +use lwk_simplicity::scripts::{create_p2tr_address, load_program}; +use lwk_simplicity::wallet_abi::schema::{ + AssetVariant, BlinderVariant, FinalizerSpec, InputSchema, InputUnblinding, InternalKeySource, + LockVariant, OutputSchema, SimfArguments, SimfWitness, UTXOSource, serialize_arguments, + serialize_witness, +}; +use lwk_wollet::elements as el26; +use simplicityhl::elements as el25; + +use crate::wallet_abi_common::{KnownUtxo, WalletAbiHarness, wallet_transfer_request}; + +#[derive(Clone, Debug)] +pub struct ScriptAuthLockState { + pub arguments: ScriptAuthArguments, + pub locked: KnownUtxo, +} + +#[derive(Clone, Debug)] +pub struct ScriptAuthUnlockState { + pub unlocked_asset: KnownUtxo, + pub auth_output: KnownUtxo, +} + +impl WalletAbiHarness { + pub async fn create_script_auth( + &self, + lock_asset_id: el26::AssetId, + lock_amount_sat: u64, + arguments: ScriptAuthArguments, + ) -> Result { + let lock_asset_id_25 = lock_asset_id + .to_string() + .parse::() + .context("failed to convert asset id from elements 0.26 to 0.25")?; + let script_auth_script_pubkey = create_p2tr_address( + load_program(SCRIPT_AUTH_SOURCE, arguments.build_script_auth_arguments())? + .commit() + .cmr(), + &InternalKeySource::Bip0341.get_x_only_pubkey(), + lwk_common::Network::LocaltestLiquid.address_params(), + ) + .script_pubkey(); + let tx = self + .process_request(wallet_transfer_request( + vec![InputSchema { + id: "locked-asset".into(), + utxo_source: UTXOSource::default(), + unblinding: InputUnblinding::Wallet, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }], + vec![OutputSchema { + id: "script-auth".into(), + amount_sat: lock_amount_sat, + lock: LockVariant::Finalizer { + finalizer: Box::new(FinalizerSpec::Simf { + source_simf: SCRIPT_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + arguments.build_script_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_script_auth_witness(&ScriptAuthWitnessParams { + input_script_index: 0, + }), + runtime_arguments: vec![], + })?, + }), + }, + asset: AssetVariant::AssetId { + asset_id: lock_asset_id, + }, + blinder: BlinderVariant::Explicit, + }], + )) + .await?; + let locked = self.find_output(&tx, |tx_out| { + tx_out.script_pubkey == script_auth_script_pubkey + && tx_out.asset.explicit() == Some(lock_asset_id_25) + && tx_out.value.explicit() == Some(lock_amount_sat) + })?; + + Ok(ScriptAuthLockState { arguments, locked }) + } + + pub async fn unlock_script_auth( + &self, + state: &ScriptAuthLockState, + auth_asset_id: el26::AssetId, + auth_amount_sat: u64, + ) -> Result { + let auth_asset_id_25 = auth_asset_id + .to_string() + .parse::() + .context("failed to convert asset id from elements 0.26 to 0.25")?; + let tx = self + .process_request(wallet_transfer_request( + vec![ + InputSchema { + id: "script-auth".into(), + utxo_source: UTXOSource::Provided { + outpoint: state.locked.outpoint, + }, + unblinding: InputUnblinding::Explicit, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Simf { + source_simf: SCRIPT_AUTH_SOURCE.to_string(), + internal_key: InternalKeySource::Bip0341, + arguments: serialize_arguments(&SimfArguments::new( + state.arguments.build_script_auth_arguments(), + ))?, + witness: serialize_witness(&SimfWitness { + resolved: build_script_auth_witness(&ScriptAuthWitnessParams { + input_script_index: 1, + }), + runtime_arguments: vec![], + })?, + }, + }, + InputSchema { + id: "auth-utxo".into(), + utxo_source: UTXOSource::default(), + unblinding: InputUnblinding::Wallet, + sequence: el26::Sequence::ENABLE_LOCKTIME_NO_RBF, + issuance: None, + finalizer: FinalizerSpec::Wallet, + }, + ], + vec![ + OutputSchema { + id: "unlocked-asset".into(), + amount_sat: state.locked.value()?, + lock: LockVariant::Script { + script: self.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: state.locked.asset_id_26()?, + }, + blinder: BlinderVariant::Explicit, + }, + OutputSchema { + id: "auth-output".into(), + amount_sat: auth_amount_sat, + lock: LockVariant::Script { + script: self.wallet_script_26().clone(), + }, + asset: AssetVariant::AssetId { + asset_id: auth_asset_id, + }, + blinder: BlinderVariant::Explicit, + }, + ], + )) + .await?; + let unlocked_asset = self.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.wallet_script_25() + && tx_out.asset.explicit() == Some(state.locked.asset_id_25().expect("asset")) + && tx_out.value.explicit() == Some(state.locked.value().expect("value")) + })?; + let auth_output = self.find_output(&tx, |tx_out| { + tx_out.script_pubkey == *self.wallet_script_25() + && tx_out.asset.explicit() == Some(auth_asset_id_25) + && tx_out.value.explicit() == Some(auth_amount_sat) + })?; + + Ok(ScriptAuthUnlockState { + unlocked_asset, + auth_output, + }) + } +} diff --git a/crates/contracts/tests/wallet_abi_asset_auth.rs b/crates/contracts/tests/wallet_abi_asset_auth.rs new file mode 100644 index 0000000..e55c3f4 --- /dev/null +++ b/crates/contracts/tests/wallet_abi_asset_auth.rs @@ -0,0 +1,95 @@ +#[path = "support/wallet_abi_asset_auth_support.rs"] +mod wallet_abi_asset_auth_support; +#[path = "support/wallet_abi_common.rs"] +mod wallet_abi_common; + +use anyhow::Result; +use lending_contracts::asset_auth::build_arguments::AssetAuthArguments; +use lwk_simplicity::wallet_abi::test_utils::{RuntimeFundingAsset, fund_address, mine_blocks}; +use wallet_abi_common::WalletAbiHarness; + +#[tokio::test] +async fn test_asset_auth_creation_happy_path() -> Result<()> { + let harness = WalletAbiHarness::new().await?; + + let _ = fund_address(&harness.signer_address, RuntimeFundingAsset::Lbtc, 10000000)?; + let auth_funding_op = fund_address( + &harness.signer_address, + RuntimeFundingAsset::IssuedAsset, + 1000, + )?; + + mine_blocks(1)?; + + let _ = harness + .create_asset_auth( + &auth_funding_op.funded_asset_id, + auth_funding_op.funded_amount_sat, + AssetAuthArguments::new( + auth_funding_op.funded_asset_id.into_inner().0, + auth_funding_op.funded_amount_sat, + false, + ), + ) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn test_asset_auth_unlock_with_burn_happy_path() -> Result<()> { + let harness = WalletAbiHarness::new().await?; + + let _ = fund_address(&harness.signer_address, RuntimeFundingAsset::Lbtc, 10000000)?; + let auth_funding_op = fund_address( + &harness.signer_address, + RuntimeFundingAsset::IssuedAsset, + 1000, + )?; + + mine_blocks(1)?; + + let mut auth_state = harness + .create_asset_auth( + &auth_funding_op.funded_asset_id, + auth_funding_op.funded_amount_sat, + AssetAuthArguments::new( + auth_funding_op.funded_asset_id.into_inner().0, + auth_funding_op.funded_amount_sat, + true, + ), + ) + .await?; + let () = harness.unlock_asset_auth(&mut auth_state).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_asset_auth_unlock_without_burn_happy_path() -> Result<()> { + let harness = WalletAbiHarness::new().await?; + + let _ = fund_address(&harness.signer_address, RuntimeFundingAsset::Lbtc, 10000000)?; + let auth_funding_op = fund_address( + &harness.signer_address, + RuntimeFundingAsset::IssuedAsset, + 1000, + )?; + + mine_blocks(1)?; + + let mut auth_state = harness + .create_asset_auth( + &auth_funding_op.funded_asset_id, + auth_funding_op.funded_amount_sat, + AssetAuthArguments::new( + auth_funding_op.funded_asset_id.into_inner().0, + auth_funding_op.funded_amount_sat, + false, + ), + ) + .await?; + let () = harness.unlock_asset_auth(&mut auth_state).await?; + + Ok(()) +} diff --git a/crates/contracts/tests/wallet_abi_lending_protocol.rs b/crates/contracts/tests/wallet_abi_lending_protocol.rs new file mode 100644 index 0000000..3654ee7 --- /dev/null +++ b/crates/contracts/tests/wallet_abi_lending_protocol.rs @@ -0,0 +1,149 @@ +#[path = "support/wallet_abi_common.rs"] +mod wallet_abi_common; +#[path = "support/wallet_abi_lending_support.rs"] +mod wallet_abi_lending_support; + +use anyhow::Result; +use wallet_abi_lending_support::LendingScenario; + +#[tokio::test] +async fn test_prepare_utility_nfts_issuance_happy_path() -> Result<()> { + let scenario = LendingScenario::new_default().await?; + let prepared = scenario.prepare_utility_nfts_issuance().await?; + + assert_eq!(prepared.issuance_utxos.len(), 4); + for issuance_utxo in &prepared.issuance_utxos { + assert_eq!(issuance_utxo.value()?, 100); + } + + Ok(()) +} + +#[tokio::test] +async fn test_issue_utility_nfts_happy_path() -> Result<()> { + let scenario = LendingScenario::new_default().await?; + let prepared = scenario.prepare_utility_nfts_issuance().await?; + let issued = scenario.issue_utility_nfts(&prepared).await?; + let (first_amount, second_amount) = scenario.terms.encode_parameters_nft_amounts(0)?; + + let borrower_asset = issued.borrower_nft.asset_id_25()?; + let lender_asset = issued.lender_nft.asset_id_25()?; + let first_asset = issued.first_parameters_nft.asset_id_25()?; + let second_asset = issued.second_parameters_nft.asset_id_25()?; + + assert_ne!(borrower_asset, lender_asset); + assert_ne!(borrower_asset, first_asset); + assert_ne!(borrower_asset, second_asset); + assert_ne!(lender_asset, first_asset); + assert_ne!(lender_asset, second_asset); + assert_ne!(first_asset, second_asset); + + assert_eq!(issued.borrower_nft.value()?, 1); + assert_eq!(issued.lender_nft.value()?, 1); + assert_eq!(issued.first_parameters_nft.value()?, first_amount); + assert_eq!(issued.second_parameters_nft.value()?, second_amount); + + Ok(()) +} + +#[tokio::test] +async fn test_pre_lock_creation_happy_path() -> Result<()> { + let scenario = LendingScenario::new_default().await?; + let prepared = scenario.prepare_utility_nfts_issuance().await?; + let issued = scenario.issue_utility_nfts(&prepared).await?; + let pre_lock = scenario.create_pre_lock(&issued).await?; + + assert_eq!(pre_lock.pre_lock.value()?, scenario.terms.collateral_amount); + assert_eq!(pre_lock.borrower_nft.value()?, 1); + assert_eq!(pre_lock.lender_nft.value()?, 1); + + Ok(()) +} + +#[tokio::test] +async fn test_pre_lock_cancellation_happy_path() -> Result<()> { + let scenario = LendingScenario::new_default().await?; + let prepared = scenario.prepare_utility_nfts_issuance().await?; + let issued = scenario.issue_utility_nfts(&prepared).await?; + let pre_lock = scenario.create_pre_lock(&issued).await?; + let collateral = scenario.cancel_pre_lock(&pre_lock).await?; + + assert_eq!(collateral.asset_id_25()?, scenario.collateral_asset_id); + assert_eq!(collateral.value()?, scenario.terms.collateral_amount); + + Ok(()) +} + +#[tokio::test] +async fn test_lending_creation_happy_path() -> Result<()> { + let scenario = LendingScenario::new_default().await?; + let prepared = scenario.prepare_utility_nfts_issuance().await?; + let issued = scenario.issue_utility_nfts(&prepared).await?; + let pre_lock = scenario.create_pre_lock(&issued).await?; + let lending = scenario.create_lending(&pre_lock).await?; + + assert_eq!(lending.lending.value()?, scenario.terms.collateral_amount); + assert_eq!( + lending.principal_borrowed.asset_id_25()?, + scenario.principal_asset_id + ); + assert_eq!( + lending.principal_borrowed.value()?, + scenario.terms.principal_amount + ); + assert_eq!(lending.borrower_nft.value()?, 1); + assert_eq!(lending.lender_nft.value()?, 1); + + Ok(()) +} + +#[tokio::test] +async fn test_loan_repayment_and_lender_claim_happy_path() -> Result<()> { + let scenario = LendingScenario::new_default().await?; + let prepared = scenario.prepare_utility_nfts_issuance().await?; + let issued = scenario.issue_utility_nfts(&prepared).await?; + let pre_lock = scenario.create_pre_lock(&issued).await?; + let lending = scenario.create_lending(&pre_lock).await?; + let repayment = scenario.repay_loan(&lending).await?; + let claim = scenario.claim_repaid_principal(&repayment).await?; + + assert_eq!( + repayment.collateral.asset_id_25()?, + scenario.collateral_asset_id + ); + assert_eq!( + repayment.collateral.value()?, + scenario.terms.collateral_amount + ); + assert_eq!( + claim.principal_claim.asset_id_25()?, + scenario.principal_asset_id + ); + assert_eq!( + claim.principal_claim.value()?, + scenario.terms.principal_with_interest() + ); + + Ok(()) +} + +#[tokio::test] +async fn test_loan_liquidation_happy_path() -> Result<()> { + let scenario = LendingScenario::new_for_liquidation().await?; + let prepared = scenario.prepare_utility_nfts_issuance().await?; + let issued = scenario.issue_utility_nfts(&prepared).await?; + let pre_lock = scenario.create_pre_lock(&issued).await?; + let lending = scenario.create_lending(&pre_lock).await?; + let liquidation = scenario.liquidate_loan(&lending).await?; + + assert_eq!( + liquidation.collateral.asset_id_25()?, + scenario.collateral_asset_id + ); + assert_eq!( + liquidation.collateral.value()?, + scenario.terms.collateral_amount + ); + + Ok(()) +} diff --git a/crates/contracts/tests/wallet_abi_script_auth.rs b/crates/contracts/tests/wallet_abi_script_auth.rs new file mode 100644 index 0000000..cc13aca --- /dev/null +++ b/crates/contracts/tests/wallet_abi_script_auth.rs @@ -0,0 +1,85 @@ +#[path = "support/wallet_abi_common.rs"] +mod wallet_abi_common; +#[path = "support/wallet_abi_script_auth_support.rs"] +mod wallet_abi_script_auth_support; + +use anyhow::Result; +use lending_contracts::script_auth::build_arguments::ScriptAuthArguments; +use lwk_simplicity::wallet_abi::test_utils::{RuntimeFundingAsset, fund_address, mine_blocks}; +use simplicityhl_core::hash_script; +use wallet_abi_common::WalletAbiHarness; + +#[tokio::test] +async fn test_script_auth_creation_happy_path() -> Result<()> { + let harness = WalletAbiHarness::new().await?; + let _ = fund_address(&harness.signer_address, RuntimeFundingAsset::Lbtc, 200_000)?; + let lock_funding = fund_address( + &harness.signer_address, + RuntimeFundingAsset::IssuedAsset, + 5_000, + )?; + mine_blocks(1)?; + let state = harness + .create_script_auth( + lock_funding.funded_asset_id, + lock_funding.funded_amount_sat, + ScriptAuthArguments::new(hash_script(harness.wallet_script_25())), + ) + .await?; + + assert_eq!(state.locked.asset_id_26()?, lock_funding.funded_asset_id); + assert_eq!(state.locked.value()?, lock_funding.funded_amount_sat); + + Ok(()) +} + +#[tokio::test] +async fn test_script_auth_unlock_happy_path() -> Result<()> { + let harness = WalletAbiHarness::new().await?; + let _ = fund_address(&harness.signer_address, RuntimeFundingAsset::Lbtc, 200_000)?; + let lock_funding = fund_address( + &harness.signer_address, + RuntimeFundingAsset::IssuedAsset, + 5_000, + )?; + mine_blocks(1)?; + let state = harness + .create_script_auth( + lock_funding.funded_asset_id, + lock_funding.funded_amount_sat, + ScriptAuthArguments::new(hash_script(harness.wallet_script_25())), + ) + .await?; + let auth_funding = fund_address( + &harness.signer_address, + RuntimeFundingAsset::IssuedAsset, + 3_000, + )?; + mine_blocks(1)?; + let unlocked = harness + .unlock_script_auth( + &state, + auth_funding.funded_asset_id, + auth_funding.funded_amount_sat, + ) + .await?; + + assert_eq!( + unlocked.unlocked_asset.asset_id_26()?, + lock_funding.funded_asset_id + ); + assert_eq!( + unlocked.unlocked_asset.value()?, + lock_funding.funded_amount_sat + ); + assert_eq!( + unlocked.auth_output.asset_id_26()?, + auth_funding.funded_asset_id + ); + assert_eq!( + unlocked.auth_output.value()?, + auth_funding.funded_amount_sat + ); + + Ok(()) +} From b454552d5f324821b4da8dbc6b6f64f8d51b5f15 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sat, 7 Mar 2026 08:24:20 +0200 Subject: [PATCH 3/7] integration tests: update CI to include regtest env setup --- .../actions/setup-regtest-execs/action.yml | 38 ++++++++++++++++ .github/workflows/tests.yml | 44 +++++++++++++++++-- .../contracts/tests/wallet_abi_asset_auth.rs | 3 ++ .../contracts/tests/wallet_abi_script_auth.rs | 11 +++++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 .github/actions/setup-regtest-execs/action.yml diff --git a/.github/actions/setup-regtest-execs/action.yml b/.github/actions/setup-regtest-execs/action.yml new file mode 100644 index 0000000..eeb73d4 --- /dev/null +++ b/.github/actions/setup-regtest-execs/action.yml @@ -0,0 +1,38 @@ +name: Setup regtest executables +description: Download and expose Liquid regtest executables for CI + +runs: + using: composite + steps: + - name: Download regtest executables + shell: bash + run: | + set -euo pipefail + + BIN_DIR="${RUNNER_TEMP}/regtest-bin" + mkdir -p "${BIN_DIR}" + cd "${BIN_DIR}" + + ELECTRS_FILENAME="electrs_linux_esplora_027e38d3ebc2f85b28ae76f8f3448438ee4fc7b1_liquid.zip" + ELECTRS_SHA256="a63a314c16bc6642fc060bbc19bd1d54ebf86b42188ff2a11c705177c1eb22f7" + if [ ! -x "${BIN_DIR}/electrs" ]; then + curl -Ls "https://github.com/RCasatta/electrsd/releases/download/electrs_releases/${ELECTRS_FILENAME}" -o "${ELECTRS_FILENAME}" + echo "${ELECTRS_SHA256} ${ELECTRS_FILENAME}" | sha256sum -c - + unzip -qo "${ELECTRS_FILENAME}" + chmod +x "${BIN_DIR}/electrs" + fi + + ELEMENTSD_VERSION="23.3.1" + ELEMENTSD_FILENAME="elements-${ELEMENTSD_VERSION}-x86_64-linux-gnu.tar.gz" + ELEMENTSD_SHA256="864e3a8240137c4e948ecae7c526ccb363771351ea68737a14c682025d5fedaa" + if [ ! -x "${BIN_DIR}/elements-${ELEMENTSD_VERSION}/bin/elementsd" ]; then + curl -Ls "https://github.com/ElementsProject/elements/releases/download/elements-${ELEMENTSD_VERSION}/${ELEMENTSD_FILENAME}" -o "${ELEMENTSD_FILENAME}" + echo "${ELEMENTSD_SHA256} ${ELEMENTSD_FILENAME}" | sha256sum -c - + tar -xzf "${ELEMENTSD_FILENAME}" + chmod +x "${BIN_DIR}/elements-${ELEMENTSD_VERSION}/bin/elementsd" + fi + + echo "${BIN_DIR}" >> "${GITHUB_PATH}" + echo "${BIN_DIR}/elements-${ELEMENTSD_VERSION}/bin" >> "${GITHUB_PATH}" + echo "ELECTRS_LIQUID_EXEC=${BIN_DIR}/electrs" >> "${GITHUB_ENV}" + echo "ELEMENTSD_EXEC=${BIN_DIR}/elements-${ELEMENTSD_VERSION}/bin/elementsd" >> "${GITHUB_ENV}" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1fc7689..576c694 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ permissions: jobs: test: - name: Build and test (matrix) + name: Build and test (matrix, non-regtest) runs-on: ubuntu-latest services: postgres: @@ -88,8 +88,46 @@ jobs: cd crates/indexer SKIP_DOCKER=true ./scripts/init_db.sh - - name: Run tests - run: cargo test --workspace --all-features --no-fail-fast --verbose + - name: Run non-regtest workspace tests + run: cargo test --workspace --all-features --exclude lending-contracts --no-fail-fast --verbose + + - name: Run lending-contracts library tests + run: cargo test -p lending-contracts --lib --all-features --verbose - name: Check that queries are fresh run: cargo sqlx prepare --workspace --check -- --all-targets + + contracts-regtest: + name: Contracts regtest tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust (stable) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + + - name: Setup regtest executables + uses: ./.github/actions/setup-regtest-execs + + - name: Test lending-contracts regtest integration tests + run: cargo test -p lending-contracts --tests --all-features --no-fail-fast --verbose -- --test-threads=1 + + - name: Assert no leaked regtest nodes after contracts tests + if: always() + run: | + set -euo pipefail + leaks="$(ps -eo pid=,comm=,args= | awk '$2=="elementsd" || $2=="electrs" {print}')" + if [ -n "$leaks" ]; then + echo "Leaked regtest node processes after contracts regtest tests:" + echo "$leaks" + exit 1 + fi diff --git a/crates/contracts/tests/wallet_abi_asset_auth.rs b/crates/contracts/tests/wallet_abi_asset_auth.rs index e55c3f4..925d76b 100644 --- a/crates/contracts/tests/wallet_abi_asset_auth.rs +++ b/crates/contracts/tests/wallet_abi_asset_auth.rs @@ -20,6 +20,7 @@ async fn test_asset_auth_creation_happy_path() -> Result<()> { )?; mine_blocks(1)?; + harness.sync_wallet().await?; let _ = harness .create_asset_auth( @@ -48,6 +49,7 @@ async fn test_asset_auth_unlock_with_burn_happy_path() -> Result<()> { )?; mine_blocks(1)?; + harness.sync_wallet().await?; let mut auth_state = harness .create_asset_auth( @@ -77,6 +79,7 @@ async fn test_asset_auth_unlock_without_burn_happy_path() -> Result<()> { )?; mine_blocks(1)?; + harness.sync_wallet().await?; let mut auth_state = harness .create_asset_auth( diff --git a/crates/contracts/tests/wallet_abi_script_auth.rs b/crates/contracts/tests/wallet_abi_script_auth.rs index cc13aca..c0fccd1 100644 --- a/crates/contracts/tests/wallet_abi_script_auth.rs +++ b/crates/contracts/tests/wallet_abi_script_auth.rs @@ -12,13 +12,17 @@ use wallet_abi_common::WalletAbiHarness; #[tokio::test] async fn test_script_auth_creation_happy_path() -> Result<()> { let harness = WalletAbiHarness::new().await?; + let _ = fund_address(&harness.signer_address, RuntimeFundingAsset::Lbtc, 200_000)?; let lock_funding = fund_address( &harness.signer_address, RuntimeFundingAsset::IssuedAsset, 5_000, )?; + mine_blocks(1)?; + harness.sync_wallet().await?; + let state = harness .create_script_auth( lock_funding.funded_asset_id, @@ -36,13 +40,17 @@ async fn test_script_auth_creation_happy_path() -> Result<()> { #[tokio::test] async fn test_script_auth_unlock_happy_path() -> Result<()> { let harness = WalletAbiHarness::new().await?; + let _ = fund_address(&harness.signer_address, RuntimeFundingAsset::Lbtc, 200_000)?; let lock_funding = fund_address( &harness.signer_address, RuntimeFundingAsset::IssuedAsset, 5_000, )?; + mine_blocks(1)?; + harness.sync_wallet().await?; + let state = harness .create_script_auth( lock_funding.funded_asset_id, @@ -55,7 +63,10 @@ async fn test_script_auth_unlock_happy_path() -> Result<()> { RuntimeFundingAsset::IssuedAsset, 3_000, )?; + mine_blocks(1)?; + harness.sync_wallet().await?; + let unlocked = harness .unlock_script_auth( &state, From c5c593b7a70f0ca62302a71962872ece2d425cbf Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Sun, 8 Mar 2026 13:29:15 +0200 Subject: [PATCH 4/7] wallet-abi web pass #1 --- crates/cli/src/commands/pre_lock.rs | 52 +- crates/contracts/src/error.rs | 12 + crates/contracts/src/pre_lock/mod.rs | 96 +- crates/contracts/src/sdk/pre_lock/creation.rs | 21 +- .../sdk/pre_lock/extract_arguments_from_tx.rs | 186 +- crates/contracts/src/sdk/pre_lock/metadata.rs | 281 ++ crates/contracts/src/sdk/pre_lock/mod.rs | 2 + .../support/wallet_abi_lending_support.rs | 19 +- crates/indexer/Cargo.toml | 2 +- crates/indexer/README.md | 3 +- crates/indexer/configuration/base.yaml | 2 +- crates/indexer/src/api/server.rs | 112 +- crates/indexer/src/main.rs | 25 +- web/.env.example | 6 + web/README.md | 30 +- web/package-lock.json | 4229 ++++++++++++++++- web/package.json | 15 +- web/src/App.tsx | 351 +- web/src/SeedContext.tsx | 19 - web/src/SeedGate.tsx | 62 - web/src/api/client.ts | 33 +- web/src/api/esplora.ts | 52 +- web/src/components/AccountMenu.tsx | 170 - web/src/components/OfferTable.tsx | 39 +- web/src/components/UtxoSelect.tsx | 78 - web/src/components/formatUtxoOptionLabel.ts | 32 - web/src/hooks/useAccountAddress.ts | 100 - web/src/pages/Borrower.tsx | 796 ++++ .../pages/CreateOffer/CancelOfferModal.tsx | 350 -- .../pages/CreateOffer/CreateOfferWizard.tsx | 191 - .../pages/CreateOffer/FinalizeOfferStep.tsx | 530 --- .../CreateOffer/IssueUtilityNftsStep.tsx | 787 --- web/src/pages/CreateOffer/PrepareStep.tsx | 300 -- web/src/pages/CreateOffer/RepaymentModal.tsx | 536 --- web/src/pages/CreateOffer/borrowerStorage.ts | 71 - web/src/pages/CreateOffer/index.tsx | 781 --- web/src/pages/Dashboard/index.tsx | 390 +- web/src/pages/Lender.tsx | 563 +++ web/src/pages/Lender/AcceptOfferModal.tsx | 463 -- web/src/pages/Lender/ClaimModal.tsx | 416 -- web/src/pages/Lender/LiquidationModal.tsx | 412 -- web/src/pages/Lender/index.tsx | 232 - web/src/pages/Utility.tsx | 869 ++++ web/src/pages/Utility/AccountSection.tsx | 258 - web/src/pages/Utility/BurnTxBuilder.tsx | 236 - web/src/pages/Utility/MergeAssetTxBuilder.tsx | 300 -- web/src/pages/Utility/MergeTxBuilder.tsx | 268 -- web/src/pages/Utility/PlaceholderBuilder.tsx | 17 - web/src/pages/Utility/SplitAssetTxBuilder.tsx | 341 -- web/src/pages/Utility/SplitTxBuilder.tsx | 271 -- web/src/pages/Utility/UtilityModeSelector.tsx | 49 - web/src/pages/Utility/index.tsx | 145 - web/src/pages/Utility/types.ts | 10 - .../simplicity/finalizeSimplicityInputs.ts | 110 - web/src/simplicity/lwk.ts | 59 +- web/src/tx/acceptOffer/buildAcceptOfferTx.ts | 188 - .../tx/acceptOffer/finalizeAcceptOfferTx.ts | 117 - .../assetAuthUnlock/buildAssetAuthUnlockTx.ts | 106 - .../finalizeAssetAuthUnlockTx.ts | 90 - web/src/tx/burn/buildBurnTx.ts | 108 - web/src/tx/burn/useBurnTxForm.ts | 306 -- .../buildIssueUtilityNftsTx.ts | 177 - .../loanLiquidation/buildLoanLiquidationTx.ts | 224 - .../finalizeLoanLiquidationTx.ts | 111 - .../tx/loanRepayment/buildLoanRepaymentTx.ts | 253 - .../loanRepayment/finalizeLoanRepaymentTx.ts | 116 - web/src/tx/merge/buildMergeTx.ts | 99 - web/src/tx/merge/useMergeAssetTxForm.ts | 414 -- web/src/tx/merge/useMergeTxForm.ts | 414 -- web/src/tx/p2pk/buildP2pkTx.ts | 91 - .../buildPreLockCancellationTx.ts | 170 - .../finalizePreLockCancellationTx.ts | 123 - .../preLockCreation/buildPreLockCreationTx.ts | 242 - .../buildPrepareUtilityNftsTx.ts | 127 - web/src/tx/psetBuilder.ts | 204 - web/src/tx/split/buildSplitAssetTx.ts | 108 - web/src/tx/split/buildSplitTx.ts | 34 - web/src/tx/split/types.ts | 16 - web/src/tx/split/useSplitAssetTxForm.ts | 401 -- web/src/tx/split/useSplitTxForm.ts | 290 -- web/src/tx/split/validation.ts | 34 - web/src/utility/addressP2pk.ts | 66 +- web/src/utility/borrowerOffers.test.ts | 85 + web/src/utility/borrowerOffers.ts | 69 + web/src/utility/buildTxOutFromPrevout.ts | 29 - web/src/utility/hex.test.ts | 50 + web/src/utility/hex.ts | 112 +- web/src/utility/preLockCovenants.ts | 82 +- .../principalWithInterest.ts | 0 web/src/utility/seed.ts | 30 - web/src/utility/signP2pkInputs.ts | 74 - web/src/utility/utxoKey.ts | 6 - web/src/walletAbi/WalletAbiSessionContext.tsx | 304 ++ web/src/walletAbi/WalletConnectCard.tsx | 57 + web/src/walletAbi/borrowerStorage.test.ts | 82 + web/src/walletAbi/borrowerStorage.ts | 49 + .../walletAbi/borrowerTrackedStorage.test.ts | 56 + web/src/walletAbi/borrowerTrackedStorage.ts | 62 + web/src/walletAbi/finalizers.ts | 140 + web/src/walletAbi/lenderStorage.test.ts | 53 + web/src/walletAbi/lenderStorage.ts | 62 + .../requestBuilders.offerMetadata.test.ts | 232 + web/src/walletAbi/requestBuilders.test.ts | 225 + web/src/walletAbi/requestBuilders.ts | 1123 +++++ web/src/walletAbi/response.ts | 16 + web/src/walletAbi/utilityRequests.test.ts | 193 + web/src/walletAbi/utilityRequests.ts | 324 ++ web/src/walletAbi/utilityStorage.ts | 138 + .../walletAbi/walletConnectSession.test.ts | 125 + web/src/walletAbi/walletConnectSession.ts | 479 ++ web/src/walletAbi/walletScriptStorage.test.ts | 61 + web/src/walletAbi/walletScriptStorage.ts | 63 + web/tsconfig.app.json | 5 + web/vite.config.ts | 4 + 114 files changed, 11771 insertions(+), 13028 deletions(-) create mode 100644 crates/contracts/src/sdk/pre_lock/metadata.rs delete mode 100644 web/src/SeedContext.tsx delete mode 100644 web/src/SeedGate.tsx delete mode 100644 web/src/components/AccountMenu.tsx delete mode 100644 web/src/components/UtxoSelect.tsx delete mode 100644 web/src/components/formatUtxoOptionLabel.ts delete mode 100644 web/src/hooks/useAccountAddress.ts create mode 100644 web/src/pages/Borrower.tsx delete mode 100644 web/src/pages/CreateOffer/CancelOfferModal.tsx delete mode 100644 web/src/pages/CreateOffer/CreateOfferWizard.tsx delete mode 100644 web/src/pages/CreateOffer/FinalizeOfferStep.tsx delete mode 100644 web/src/pages/CreateOffer/IssueUtilityNftsStep.tsx delete mode 100644 web/src/pages/CreateOffer/PrepareStep.tsx delete mode 100644 web/src/pages/CreateOffer/RepaymentModal.tsx delete mode 100644 web/src/pages/CreateOffer/borrowerStorage.ts delete mode 100644 web/src/pages/CreateOffer/index.tsx create mode 100644 web/src/pages/Lender.tsx delete mode 100644 web/src/pages/Lender/AcceptOfferModal.tsx delete mode 100644 web/src/pages/Lender/ClaimModal.tsx delete mode 100644 web/src/pages/Lender/LiquidationModal.tsx delete mode 100644 web/src/pages/Lender/index.tsx create mode 100644 web/src/pages/Utility.tsx delete mode 100644 web/src/pages/Utility/AccountSection.tsx delete mode 100644 web/src/pages/Utility/BurnTxBuilder.tsx delete mode 100644 web/src/pages/Utility/MergeAssetTxBuilder.tsx delete mode 100644 web/src/pages/Utility/MergeTxBuilder.tsx delete mode 100644 web/src/pages/Utility/PlaceholderBuilder.tsx delete mode 100644 web/src/pages/Utility/SplitAssetTxBuilder.tsx delete mode 100644 web/src/pages/Utility/SplitTxBuilder.tsx delete mode 100644 web/src/pages/Utility/UtilityModeSelector.tsx delete mode 100644 web/src/pages/Utility/index.tsx delete mode 100644 web/src/pages/Utility/types.ts delete mode 100644 web/src/simplicity/finalizeSimplicityInputs.ts delete mode 100644 web/src/tx/acceptOffer/buildAcceptOfferTx.ts delete mode 100644 web/src/tx/acceptOffer/finalizeAcceptOfferTx.ts delete mode 100644 web/src/tx/assetAuthUnlock/buildAssetAuthUnlockTx.ts delete mode 100644 web/src/tx/assetAuthUnlock/finalizeAssetAuthUnlockTx.ts delete mode 100644 web/src/tx/burn/buildBurnTx.ts delete mode 100644 web/src/tx/burn/useBurnTxForm.ts delete mode 100644 web/src/tx/issueUtilityNfts/buildIssueUtilityNftsTx.ts delete mode 100644 web/src/tx/loanLiquidation/buildLoanLiquidationTx.ts delete mode 100644 web/src/tx/loanLiquidation/finalizeLoanLiquidationTx.ts delete mode 100644 web/src/tx/loanRepayment/buildLoanRepaymentTx.ts delete mode 100644 web/src/tx/loanRepayment/finalizeLoanRepaymentTx.ts delete mode 100644 web/src/tx/merge/buildMergeTx.ts delete mode 100644 web/src/tx/merge/useMergeAssetTxForm.ts delete mode 100644 web/src/tx/merge/useMergeTxForm.ts delete mode 100644 web/src/tx/p2pk/buildP2pkTx.ts delete mode 100644 web/src/tx/preLockCancellation/buildPreLockCancellationTx.ts delete mode 100644 web/src/tx/preLockCancellation/finalizePreLockCancellationTx.ts delete mode 100644 web/src/tx/preLockCreation/buildPreLockCreationTx.ts delete mode 100644 web/src/tx/prepareUtilityNfts/buildPrepareUtilityNftsTx.ts delete mode 100644 web/src/tx/psetBuilder.ts delete mode 100644 web/src/tx/split/buildSplitAssetTx.ts delete mode 100644 web/src/tx/split/buildSplitTx.ts delete mode 100644 web/src/tx/split/types.ts delete mode 100644 web/src/tx/split/useSplitAssetTxForm.ts delete mode 100644 web/src/tx/split/useSplitTxForm.ts delete mode 100644 web/src/tx/split/validation.ts create mode 100644 web/src/utility/borrowerOffers.test.ts create mode 100644 web/src/utility/borrowerOffers.ts delete mode 100644 web/src/utility/buildTxOutFromPrevout.ts create mode 100644 web/src/utility/hex.test.ts rename web/src/{tx/loanRepayment => utility}/principalWithInterest.ts (100%) delete mode 100644 web/src/utility/seed.ts delete mode 100644 web/src/utility/signP2pkInputs.ts delete mode 100644 web/src/utility/utxoKey.ts create mode 100644 web/src/walletAbi/WalletAbiSessionContext.tsx create mode 100644 web/src/walletAbi/WalletConnectCard.tsx create mode 100644 web/src/walletAbi/borrowerStorage.test.ts create mode 100644 web/src/walletAbi/borrowerStorage.ts create mode 100644 web/src/walletAbi/borrowerTrackedStorage.test.ts create mode 100644 web/src/walletAbi/borrowerTrackedStorage.ts create mode 100644 web/src/walletAbi/finalizers.ts create mode 100644 web/src/walletAbi/lenderStorage.test.ts create mode 100644 web/src/walletAbi/lenderStorage.ts create mode 100644 web/src/walletAbi/requestBuilders.offerMetadata.test.ts create mode 100644 web/src/walletAbi/requestBuilders.test.ts create mode 100644 web/src/walletAbi/requestBuilders.ts create mode 100644 web/src/walletAbi/response.ts create mode 100644 web/src/walletAbi/utilityRequests.test.ts create mode 100644 web/src/walletAbi/utilityRequests.ts create mode 100644 web/src/walletAbi/utilityStorage.ts create mode 100644 web/src/walletAbi/walletConnectSession.test.ts create mode 100644 web/src/walletAbi/walletConnectSession.ts create mode 100644 web/src/walletAbi/walletScriptStorage.test.ts create mode 100644 web/src/walletAbi/walletScriptStorage.ts diff --git a/crates/cli/src/commands/pre_lock.rs b/crates/cli/src/commands/pre_lock.rs index 0f08b79..e68c9c4 100644 --- a/crates/cli/src/commands/pre_lock.rs +++ b/crates/cli/src/commands/pre_lock.rs @@ -24,7 +24,7 @@ use lending_contracts::sdk::parameters::{ FirstNFTParameters, LendingParameters, SecondNFTParameters, }; -use lending_contracts::sdk::taproot_unspendable_internal_key; +use lending_contracts::sdk::{decode_pre_lock_metadata, taproot_unspendable_internal_key}; use simplicity_contracts::sdk::validation::TxOutExt; use simplicityhl::elements::bitcoin::secp256k1; use simplicityhl::elements::hashes::Hash; @@ -37,7 +37,7 @@ use simplicityhl::simplicity::hex::DisplayHex; use simplicityhl::tracker::TrackerLogLevel; use simplicity_contracts::sdk::taproot_pubkey_gen::get_random_seed; -use simplicity_contracts_cli::explorer::{broadcast_tx, fetch_utxo}; +use simplicity_contracts_cli::explorer::{ExplorerError, broadcast_tx, fetch_utxo}; use simplicity_contracts_cli::modules::utils::derive_keypair; use simplicityhl_core::{ @@ -289,6 +289,7 @@ impl PreLock { let borrower_nft_utxo = OutPoint::new(*pre_lock_tx_id, 3); let lender_nft_utxo = OutPoint::new(*pre_lock_tx_id, 4); let op_return_utxo = OutPoint::new(*pre_lock_tx_id, 5); + let borrower_output_script_hash_utxo = OutPoint::new(*pre_lock_tx_id, 6); let pre_lock_tx_out = fetch_utxo(pre_lock_utxo).await?; let first_parameters_nft_tx_out = fetch_utxo(first_parameters_nft_utxo).await?; @@ -296,6 +297,12 @@ impl PreLock { let borrower_nft_tx_out = fetch_utxo(borrower_nft_utxo).await?; let lender_nft_tx_out = fetch_utxo(lender_nft_utxo).await?; let op_return_tx_out = fetch_utxo(op_return_utxo).await?; + let borrower_output_script_hash_tx_out = + match fetch_utxo(borrower_output_script_hash_utxo).await { + Ok(tx_out) => Some(tx_out), + Err(ExplorerError::OutputIndexOutOfBounds { .. }) => None, + Err(err) => return Err(err.into()), + }; let (pre_lock_asset_id, _) = pre_lock_tx_out.explicit()?; let (first_parameters_nft_asset_id, first_parameters_nft_value) = @@ -325,17 +332,31 @@ impl PreLock { .push_bytes() .unwrap(); - let (op_return_pub_key, op_return_asset_id) = op_return_bytes.split_at(32); - - let principal_asset_id: [u8; 32] = - op_return_asset_id.try_into().expect("Length must be 32"); - - let borrower_public_key = XOnlyPublicKey::from_slice(op_return_pub_key).unwrap(); + let borrower_output_script_hash_bytes = borrower_output_script_hash_tx_out + .as_ref() + .filter(|tx_out| tx_out.is_null_data()) + .and_then(|tx_out| { + let mut op_return_instr_iter = tx_out.script_pubkey.instructions_minimal(); + let _ = op_return_instr_iter.next()?; + op_return_instr_iter + .next() + .and_then(Result::ok) + .and_then(|instruction| instruction.push_bytes()) + }); + + let metadata = + decode_pre_lock_metadata(op_return_bytes, borrower_output_script_hash_bytes)?; + let principal_asset_id = metadata.principal_asset_id(); + let borrower_public_key = + XOnlyPublicKey::from_slice(&metadata.borrower_pub_key()).unwrap(); println!("Pre Lock covenant info:"); println!("Assets Info:"); println!("\tCollateral asset id: {}", pre_lock_asset_id.to_hex()); - println!("\tPrincipal asset id: {}", principal_asset_id.to_hex()); + println!( + "\tPrincipal asset id: {}", + AssetId::from_slice(&principal_asset_id)?.to_hex() + ); println!( "\tFirst Parameters NFT asset id: {}", first_parameters_nft_asset_id.to_hex() @@ -351,6 +372,18 @@ impl PreLock { println!("\tLender NFT asset id: {}", lender_nft_asset_id.to_hex()); println!("Lending Offer Info:"); println!("\tBorrower public key: {borrower_public_key}"); + if let Some(borrower_output_script) = metadata.borrower_output_script() { + println!( + "\tBorrower output script: {}", + borrower_output_script.to_hex() + ); + } + if let Some(borrower_output_script_hash) = metadata.borrower_output_script_hash() { + println!( + "\tBorrower output script hash: {}", + borrower_output_script_hash.to_hex() + ); + } println!("\tCollateral amount: {}", lending_params.collateral_amount); println!("\tPrincipal amount: {}", lending_params.principal_amount); println!( @@ -771,6 +804,7 @@ impl PreLock { (*lender_nft_utxo, lender_nft_tx_out.clone()), (*fee_utxo, fee_tx_out.clone()), &pre_lock_arguments, + Some(&to_address.script_pubkey()), *fee_amount, NETWORK, )?; diff --git a/crates/contracts/src/error.rs b/crates/contracts/src/error.rs index db76954..de91a70 100644 --- a/crates/contracts/src/error.rs +++ b/crates/contracts/src/error.rs @@ -37,6 +37,18 @@ pub enum PreLockError { NotAPreLockTransaction { txid: String }, #[error("Invalid OP_RETURN metadata bytes: {bytes}")] InvalidOpReturnBytes { bytes: String }, + #[error( + "Pre lock borrower output script hashes differ: borrower NFT {borrower_nft_output_script_hash}, principal {principal_output_script_hash}" + )] + InconsistentBorrowerOutputScriptHashes { + borrower_nft_output_script_hash: String, + principal_output_script_hash: String, + }, + #[error("Borrower output script hash mismatch: expected {expected_hash}, actual {actual_hash}")] + BorrowerOutputScriptHashMismatch { + expected_hash: String, + actual_hash: String, + }, } /// Errors from transaction building operations. diff --git a/crates/contracts/src/pre_lock/mod.rs b/crates/contracts/src/pre_lock/mod.rs index dee08a8..82662af 100644 --- a/crates/contracts/src/pre_lock/mod.rs +++ b/crates/contracts/src/pre_lock/mod.rs @@ -140,7 +140,7 @@ mod pre_lock_tests { use crate::sdk::parameters::LendingParameters; use crate::sdk::{ build_pre_lock_cancellation, build_pre_lock_creation, build_pre_lock_lending_creation, - taproot_unspendable_internal_key, + decode_pre_lock_metadata, extract_arguments_from_tx, taproot_unspendable_internal_key, }; use super::*; @@ -178,6 +178,7 @@ mod pre_lock_tests { second_parameters_nft_amount: u64, borrower_pub_key: &XOnlyPublicKey, lending_params: &LendingParameters, + borrower_output_script_hash: Option<[u8; 32]>, ) -> Result<((PartiallySignedTransaction, Address), PreLockArguments)> { // Calculate script hash for the AssetAuth covenant with the Lender NFT auth let asset_auth_arguments = AssetAuthArguments { @@ -222,9 +223,9 @@ mod pre_lock_tests { .script_pubkey(); let parameters_nft_output_script_hash = hash_script(&script_auth_script); - // Calculate P2TR script hash - let borrower_p2tr_address = get_p2pk_address(borrower_pub_key, NETWORK)?; - let borrower_p2tr_script_hash = hash_script(&borrower_p2tr_address.script_pubkey()); + let borrower_p2tr_script_hash = borrower_output_script_hash.unwrap_or(hash_script( + &get_p2pk_address(borrower_pub_key, NETWORK)?.script_pubkey(), + )); let pre_lock_arguments = PreLockArguments::new( collateral_asset_id.into_inner().0, @@ -304,6 +305,7 @@ mod pre_lock_tests { }, ), &pre_lock_arguments, + None, 100, NETWORK, )?, @@ -369,6 +371,7 @@ mod pre_lock_tests { second_parameters_amount, &test_borrower_key, &lending_params, + None, )?; let pst = pst.extract_tx()?; @@ -389,15 +392,86 @@ mod pre_lock_tests { .push_bytes() .unwrap(); - let (op_return_pub_key, op_return_asset_id) = op_return_bytes.split_at(32); - - let op_return_asset_id: [u8; 32] = - op_return_asset_id.try_into().expect("Length must be 32"); + let borrower_output_script_hash_bytes = pst.output.get(6).and_then(|tx_out| { + if !tx_out.is_null_data() { + return None; + } + + let mut op_return_instr_iter = tx_out.script_pubkey.instructions_minimal(); + let _ = op_return_instr_iter.next()?; + op_return_instr_iter + .next() + .and_then(Result::ok) + .and_then(|instruction| instruction.push_bytes()) + }); - let op_return_public_key = XOnlyPublicKey::from_slice(op_return_pub_key).unwrap(); + let metadata = + decode_pre_lock_metadata(op_return_bytes, borrower_output_script_hash_bytes)?; + let op_return_public_key = + XOnlyPublicKey::from_slice(&metadata.borrower_pub_key()).unwrap(); + let borrower_p2tr_script_hash = + hash_script(&get_p2pk_address(&test_borrower_key, NETWORK)?.script_pubkey()); assert!(op_return_public_key.serialize() == test_borrower_key.serialize()); - assert!(principal_asset_id.into_inner().0 == op_return_asset_id); + assert!(principal_asset_id.into_inner().0 == metadata.principal_asset_id()); + assert_eq!( + metadata.borrower_output_script_hash(), + Some(borrower_p2tr_script_hash) + ); + + Ok(()) + } + + #[test] + fn test_pre_lock_creation_round_trips_custom_borrower_output_script_hash() -> Result<()> { + let keypair = Keypair::from_secret_key( + &Secp256k1::new(), + &secp256k1::SecretKey::from_slice(&[1u8; 32])?, + ); + let test_borrower_key = keypair.x_only_public_key().0; + + let principal_asset_id = AssetId::from_str(LIQUID_TESTNET_TEST_ASSET_ID_STR)?; + let ( + first_parameters_nft_asset_id, + second_parameters_nft_asset_id, + borrower_nft_asset_id, + lender_nft_asset_id, + ) = create_test_assets()?; + + let lending_params = LendingParameters { + collateral_amount: 10_000, + principal_amount: 4_000, + loan_expiration_time: 100, + principal_interest_rate: 250, + }; + let (first_parameters_amount, second_parameters_amount) = + lending_params.encode_parameters_nft_amounts(2)?; + let borrower_output_script_hash = hash_script(&Script::new_p2pkh(&PubkeyHash::hash( + &test_borrower_key.serialize(), + ))); + + let ((pst, _), pre_lock_arguments) = get_creation_pst( + *LIQUID_TESTNET_BITCOIN_ASSET, + principal_asset_id, + first_parameters_nft_asset_id, + second_parameters_nft_asset_id, + borrower_nft_asset_id, + lender_nft_asset_id, + first_parameters_amount, + second_parameters_amount, + &test_borrower_key, + &lending_params, + Some(borrower_output_script_hash), + )?; + + let tx = pst.extract_tx()?; + let extracted_arguments = extract_arguments_from_tx(&tx, NETWORK)?; + + assert_eq!(extracted_arguments, pre_lock_arguments); + assert_eq!( + extracted_arguments.principal_output_script_hash(), + borrower_output_script_hash + ); Ok(()) } @@ -439,6 +513,7 @@ mod pre_lock_tests { second_parameters_amount, &test_borrower_key, &lending_params, + None, )?; let pst = pst.extract_tx()?; @@ -612,6 +687,7 @@ mod pre_lock_tests { second_parameters_amount, &test_borrower_key, &lending_params, + None, )?; let pst = pst.extract_tx()?; diff --git a/crates/contracts/src/sdk/pre_lock/creation.rs b/crates/contracts/src/sdk/pre_lock/creation.rs index 421f06f..02dd2ce 100644 --- a/crates/contracts/src/sdk/pre_lock/creation.rs +++ b/crates/contracts/src/sdk/pre_lock/creation.rs @@ -14,6 +14,7 @@ use crate::script_auth::build_arguments::ScriptAuthArguments; use crate::script_auth::get_script_auth_address; use crate::sdk::basic::{add_base_input_from_utxo, check_asset_id, check_asset_value}; use crate::sdk::parameters::{FirstNFTParameters, SecondNFTParameters}; +use crate::sdk::pre_lock::metadata::PreLockMetadata; use crate::sdk::taproot_unspendable_internal_key; /// Create a new pre lock contract. @@ -40,6 +41,7 @@ pub fn build_pre_lock_creation( lender_nft_utxo: (OutPoint, TxOut), fee_utxo: (OutPoint, TxOut), pre_lock_arguments: &PreLockArguments, + borrower_output_script: Option<&Script>, fee_amount: u64, network: SimplicityNetwork, ) -> Result<(PartiallySignedTransaction, Address), TransactionBuildError> { @@ -192,10 +194,12 @@ pub fn build_pre_lock_creation( None, )); - // Add OP_RETURN output with the Borrower public key and the Principal asset id - let mut op_return_data = [0u8; 64]; - op_return_data[..32].copy_from_slice(&pre_lock_arguments.borrower_pub_key()); - op_return_data[32..].copy_from_slice(&pre_lock_arguments.principal_asset_id()); + // Persist the borrower signing key and principal asset in the primary metadata output. + let metadata = PreLockMetadata::from_pre_lock_arguments( + pre_lock_arguments, + borrower_output_script.map(simplicityhl::elements::Script::as_bytes), + )?; + let op_return_data = metadata.encode(); pst.add_output(Output::new_explicit( Script::new_op_return(&op_return_data), @@ -204,6 +208,15 @@ pub fn build_pre_lock_creation( None, )); + if let Some(borrower_output_metadata) = metadata.encode_borrower_output_metadata() { + pst.add_output(Output::new_explicit( + Script::new_op_return(&borrower_output_metadata), + 0, + AssetId::default(), + None, + )); + } + // Return collateral asset change if is_collateral_change_needed { pst.add_output(Output::new_explicit( diff --git a/crates/contracts/src/sdk/pre_lock/extract_arguments_from_tx.rs b/crates/contracts/src/sdk/pre_lock/extract_arguments_from_tx.rs index 438e72d..fc7af1b 100644 --- a/crates/contracts/src/sdk/pre_lock/extract_arguments_from_tx.rs +++ b/crates/contracts/src/sdk/pre_lock/extract_arguments_from_tx.rs @@ -1,7 +1,9 @@ use simplicity_contracts::sdk::validation::TxOutExt; use simplicityhl::elements::Transaction; use simplicityhl::elements::hex::ToHex; +use simplicityhl::elements::opcodes; use simplicityhl::elements::schnorr::XOnlyPublicKey; +use simplicityhl::elements::script::Instruction; use simplicityhl_core::{SimplicityNetwork, get_p2pk_address, hash_script}; use crate::asset_auth::build_arguments::AssetAuthArguments; @@ -13,8 +15,39 @@ use crate::pre_lock::build_arguments::PreLockArguments; use crate::script_auth::build_arguments::ScriptAuthArguments; use crate::script_auth::get_script_auth_address; use crate::sdk::parameters::{FirstNFTParameters, LendingParameters, SecondNFTParameters}; +use crate::sdk::pre_lock::metadata::decode_pre_lock_metadata; use crate::sdk::taproot_unspendable_internal_key; +fn extract_null_data_bytes( + script: &simplicityhl::elements::Script, +) -> Result<&[u8], TransactionBuildError> { + let mut script_instr_iter = script.instructions_minimal(); + + match script_instr_iter.next() { + Some(Ok(Instruction::Op(opcodes::all::OP_RETURN))) => {} + _ => { + return Err(TransactionBuildError::PreLock( + PreLockError::InvalidOpReturnBytes { + bytes: script.to_hex(), + }, + )); + } + } + + match script_instr_iter.next() { + Some(Ok(push_instruction)) => push_instruction.push_bytes().ok_or_else(|| { + TransactionBuildError::PreLock(PreLockError::InvalidOpReturnBytes { + bytes: script.to_hex(), + }) + }), + _ => Err(TransactionBuildError::PreLock( + PreLockError::InvalidOpReturnBytes { + bytes: script.to_hex(), + }, + )), + } +} + /// Extract a pre lock arguments from the pre lock creation transaction /// /// # Errors @@ -25,36 +58,33 @@ use crate::sdk::taproot_unspendable_internal_key; /// - Passed UTXOs asset ids and values differ from the arguments /// - Covenants addresses getting fails /// -/// # Panics -/// -/// - if `OP_RETURN` UTXO has invalid bytes #[allow(clippy::too_many_lines)] pub fn extract_arguments_from_tx( tx: &Transaction, network: SimplicityNetwork, ) -> Result { - if tx.input.len() < 6 || tx.output.len() < 7 { - return Err(TransactionBuildError::PreLock( - PreLockError::NotAPreLockTransaction { - txid: tx.txid().to_hex(), - }, - )); + let not_a_pre_lock = || { + TransactionBuildError::PreLock(PreLockError::NotAPreLockTransaction { + txid: tx.txid().to_hex(), + }) + }; + + // Wallet ABI pre-lock creation can use 5 inputs when the collateral asset is the + // policy asset and the same LBTC input covers both collateral and fees. Non-policy + // collateral requires an additional LBTC fee input, so 6+ inputs remain valid too. + if tx.input.len() < 5 || tx.output.len() < 7 { + return Err(not_a_pre_lock()); } - // Unwrap is safe here because we have already checked outputs length - let pre_lock_tx_out = tx.output.first().unwrap(); - let first_parameters_nft_tx_out = tx.output.get(1).unwrap(); - let second_parameters_nft_tx_out = tx.output.get(2).unwrap(); - let borrower_nft_tx_out = tx.output.get(3).unwrap(); - let lender_nft_tx_out = tx.output.get(4).unwrap(); - let op_return_tx_out = tx.output.get(5).unwrap(); + let pre_lock_tx_out = tx.output.first().ok_or_else(not_a_pre_lock)?; + let first_parameters_nft_tx_out = tx.output.get(1).ok_or_else(not_a_pre_lock)?; + let second_parameters_nft_tx_out = tx.output.get(2).ok_or_else(not_a_pre_lock)?; + let borrower_nft_tx_out = tx.output.get(3).ok_or_else(not_a_pre_lock)?; + let lender_nft_tx_out = tx.output.get(4).ok_or_else(not_a_pre_lock)?; + let op_return_tx_out = tx.output.get(5).ok_or_else(not_a_pre_lock)?; if !op_return_tx_out.is_null_data() { - return Err(TransactionBuildError::PreLock( - PreLockError::NotAPreLockTransaction { - txid: tx.txid().to_hex(), - }, - )); + return Err(not_a_pre_lock()); } let (pre_lock_asset_id, _) = pre_lock_tx_out.explicit()?; @@ -71,31 +101,22 @@ pub fn extract_arguments_from_tx( let lending_params = LendingParameters::build_from_parameters_nfts(&first_parameters, &second_parameters); - let mut op_return_instr_iter = op_return_tx_out.script_pubkey.instructions_minimal(); - - op_return_instr_iter.next(); - - let op_return_bytes = op_return_instr_iter - .next() - .unwrap() - .unwrap() - .push_bytes() - .unwrap(); - - let (op_return_pub_key, op_return_asset_id) = op_return_bytes.split_at(32); - - let principal_asset_id: [u8; 32] = - op_return_asset_id - .try_into() - .map_err(|_| PreLockError::InvalidOpReturnBytes { + let op_return_bytes = extract_null_data_bytes(&op_return_tx_out.script_pubkey)?; + let borrower_output_script_hash_bytes = tx + .output + .get(6) + .filter(|tx_out| tx_out.is_null_data()) + .map(|tx_out| extract_null_data_bytes(&tx_out.script_pubkey)) + .transpose()?; + + let metadata = decode_pre_lock_metadata(op_return_bytes, borrower_output_script_hash_bytes)?; + let principal_asset_id = metadata.principal_asset_id(); + let borrower_public_key = + XOnlyPublicKey::from_slice(&metadata.borrower_pub_key()).map_err(|_| { + PreLockError::InvalidOpReturnBytes { bytes: op_return_bytes.to_hex(), - })?; - - let borrower_public_key = XOnlyPublicKey::from_slice(op_return_pub_key).map_err(|_| { - PreLockError::InvalidOpReturnBytes { - bytes: op_return_bytes.to_hex(), - } - })?; + } + })?; // Calculate script hash for the AssetAuth covenant with the Lender NFT auth let asset_auth_arguments = AssetAuthArguments { @@ -140,9 +161,13 @@ pub fn extract_arguments_from_tx( .script_pubkey(); let parameters_nft_output_script_hash = hash_script(&script_auth_script); - // Calculate P2TR script hash - let borrower_p2tr_address = get_p2pk_address(&borrower_public_key, network)?; - let borrower_p2tr_script_hash = hash_script(&borrower_p2tr_address.script_pubkey()); + let borrower_output_script_hash = + if let Some(borrower_output_script_hash) = metadata.borrower_output_script_hash() { + borrower_output_script_hash + } else { + let borrower_p2tr_address = get_p2pk_address(&borrower_public_key, network)?; + hash_script(&borrower_p2tr_address.script_pubkey()) + }; let pre_lock_arguments = PreLockArguments::new( pre_lock_asset_id.into_inner().0, @@ -153,11 +178,72 @@ pub fn extract_arguments_from_tx( second_parameters_nft_asset_id.into_inner().0, lending_cov_hash, parameters_nft_output_script_hash, - borrower_p2tr_script_hash, - borrower_p2tr_script_hash, + borrower_output_script_hash, + borrower_output_script_hash, borrower_public_key.serialize(), &lending_params, ); Ok(pre_lock_arguments) } + +#[cfg(test)] +mod tests { + use super::*; + use simplicityhl::elements::AssetId; + use simplicityhl::elements::LockTime; + use simplicityhl::elements::OutPoint; + use simplicityhl::elements::Script; + use simplicityhl::elements::Sequence; + use simplicityhl::elements::TxIn; + use simplicityhl::elements::TxOut; + use simplicityhl::elements::TxOutWitness; + use simplicityhl::elements::confidential::{Asset, Nonce, Value}; + + fn explicit_tx_out(asset_id: AssetId, amount: u64, script_pubkey: Script) -> TxOut { + TxOut { + asset: Asset::Explicit(asset_id), + value: Value::Explicit(amount), + nonce: Nonce::Null, + script_pubkey, + witness: TxOutWitness::default(), + } + } + + #[test] + fn rejects_short_op_return_metadata_without_panicking() { + let asset_id = AssetId::default(); + let tx = Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![ + TxIn { + previous_output: OutPoint::default(), + is_pegin: false, + script_sig: Script::new(), + sequence: Sequence::MAX, + asset_issuance: simplicityhl::elements::AssetIssuance::default(), + witness: simplicityhl::elements::TxInWitness::default(), + }; + 6 + ], + output: vec![ + explicit_tx_out(asset_id, 10, Script::new()), + explicit_tx_out(asset_id, 11, Script::new()), + explicit_tx_out(asset_id, 12, Script::new()), + explicit_tx_out(asset_id, 1, Script::new()), + explicit_tx_out(asset_id, 1, Script::new()), + explicit_tx_out(asset_id, 0, Script::new_op_return(&[1u8; 31])), + explicit_tx_out(asset_id, 1, Script::new()), + ], + }; + + let error = extract_arguments_from_tx(&tx, SimplicityNetwork::LiquidTestnet) + .expect_err("short OP_RETURN metadata should be rejected"); + + match error { + TransactionBuildError::PreLock(PreLockError::InvalidOpReturnBytes { .. }) => {} + other => panic!("unexpected error: {other:?}"), + } + } +} diff --git a/crates/contracts/src/sdk/pre_lock/metadata.rs b/crates/contracts/src/sdk/pre_lock/metadata.rs new file mode 100644 index 0000000..8660aa2 --- /dev/null +++ b/crates/contracts/src/sdk/pre_lock/metadata.rs @@ -0,0 +1,281 @@ +use sha2::{Digest, Sha256}; +use simplicityhl::elements::hex::ToHex; + +use crate::error::PreLockError; +use crate::pre_lock::build_arguments::PreLockArguments; + +pub const PRE_LOCK_METADATA_LEN: usize = 64; + +#[derive(Debug, Clone, PartialEq, Eq)] +enum BorrowerOutputMetadata { + Hash([u8; 32]), + Script(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreLockMetadata { + borrower_pub_key: [u8; 32], + principal_asset_id: [u8; 32], + borrower_output_metadata: Option, +} + +impl PreLockMetadata { + #[must_use] + pub const fn new_legacy(borrower_pub_key: [u8; 32], principal_asset_id: [u8; 32]) -> Self { + Self { + borrower_pub_key, + principal_asset_id, + borrower_output_metadata: None, + } + } + + #[must_use] + pub const fn new_with_hash( + borrower_pub_key: [u8; 32], + principal_asset_id: [u8; 32], + borrower_output_script_hash: [u8; 32], + ) -> Self { + Self { + borrower_pub_key, + principal_asset_id, + borrower_output_metadata: Some(BorrowerOutputMetadata::Hash( + borrower_output_script_hash, + )), + } + } + + #[must_use] + pub fn new_with_script( + borrower_pub_key: [u8; 32], + principal_asset_id: [u8; 32], + borrower_output_script: Vec, + ) -> Self { + Self { + borrower_pub_key, + principal_asset_id, + borrower_output_metadata: Some(BorrowerOutputMetadata::Script(borrower_output_script)), + } + } + + /// Builds pre-lock metadata from canonical pre-lock arguments. + /// + /// # Errors + /// + /// Returns an error when the borrower NFT and principal outputs do not share the + /// same script hash, or when a provided borrower output script does not match that hash. + pub fn from_pre_lock_arguments( + pre_lock_arguments: &PreLockArguments, + borrower_output_script: Option<&[u8]>, + ) -> Result { + let borrower_nft_output_script_hash = pre_lock_arguments.borrower_nft_output_script_hash(); + let principal_output_script_hash = pre_lock_arguments.principal_output_script_hash(); + + if borrower_nft_output_script_hash != principal_output_script_hash { + return Err(PreLockError::InconsistentBorrowerOutputScriptHashes { + borrower_nft_output_script_hash: borrower_nft_output_script_hash.to_hex(), + principal_output_script_hash: principal_output_script_hash.to_hex(), + }); + } + + if let Some(borrower_output_script) = borrower_output_script { + let actual_hash = hash_script_bytes(borrower_output_script); + if actual_hash != principal_output_script_hash { + return Err(PreLockError::BorrowerOutputScriptHashMismatch { + expected_hash: principal_output_script_hash.to_hex(), + actual_hash: actual_hash.to_hex(), + }); + } + + return Ok(Self::new_with_script( + pre_lock_arguments.borrower_pub_key(), + pre_lock_arguments.principal_asset_id(), + borrower_output_script.to_vec(), + )); + } + + Ok(Self::new_with_hash( + pre_lock_arguments.borrower_pub_key(), + pre_lock_arguments.principal_asset_id(), + principal_output_script_hash, + )) + } + + #[must_use] + pub const fn borrower_pub_key(&self) -> [u8; 32] { + self.borrower_pub_key + } + + #[must_use] + pub const fn principal_asset_id(&self) -> [u8; 32] { + self.principal_asset_id + } + + #[must_use] + pub fn borrower_output_script_hash(&self) -> Option<[u8; 32]> { + match &self.borrower_output_metadata { + Some(BorrowerOutputMetadata::Hash(hash)) => Some(*hash), + Some(BorrowerOutputMetadata::Script(script)) => Some(hash_script_bytes(script)), + None => None, + } + } + + #[must_use] + pub fn borrower_output_script(&self) -> Option<&[u8]> { + match &self.borrower_output_metadata { + Some(BorrowerOutputMetadata::Script(script)) => Some(script.as_slice()), + _ => None, + } + } + + #[must_use] + pub fn encode(&self) -> [u8; PRE_LOCK_METADATA_LEN] { + let mut bytes = [0u8; PRE_LOCK_METADATA_LEN]; + bytes[..32].copy_from_slice(&self.borrower_pub_key); + bytes[32..].copy_from_slice(&self.principal_asset_id); + bytes + } + + #[must_use] + pub fn encode_borrower_output_metadata(&self) -> Option> { + match &self.borrower_output_metadata { + Some(BorrowerOutputMetadata::Hash(hash)) => Some(hash.to_vec()), + Some(BorrowerOutputMetadata::Script(script)) => Some(script.clone()), + None => None, + } + } +} + +fn hash_script_bytes(bytes: &[u8]) -> [u8; 32] { + let digest = Sha256::digest(bytes); + let mut hash = [0u8; 32]; + hash.copy_from_slice(&digest); + hash +} + +/// Decodes the `OP_RETURN` metadata emitted by the pre-lock creation transaction. +/// +/// # Errors +/// +/// Returns an error when the primary metadata length is invalid or when the optional +/// borrower output metadata is malformed. +pub fn decode_pre_lock_metadata( + bytes: &[u8], + borrower_output_metadata_bytes: Option<&[u8]>, +) -> Result { + if bytes.len() != PRE_LOCK_METADATA_LEN { + return Err(PreLockError::InvalidOpReturnBytes { + bytes: bytes.to_hex(), + }); + } + + let borrower_pub_key = + bytes[..32] + .try_into() + .map_err(|_| PreLockError::InvalidOpReturnBytes { + bytes: bytes.to_hex(), + })?; + let principal_asset_id = + bytes[32..] + .try_into() + .map_err(|_| PreLockError::InvalidOpReturnBytes { + bytes: bytes.to_hex(), + })?; + + let borrower_output_metadata = borrower_output_metadata_bytes + .map(|bytes| { + if bytes.len() == 32 { + bytes + .try_into() + .map(BorrowerOutputMetadata::Hash) + .map_err(|_| PreLockError::InvalidOpReturnBytes { + bytes: bytes.to_hex(), + }) + } else if bytes.is_empty() { + Err(PreLockError::InvalidOpReturnBytes { + bytes: bytes.to_hex(), + }) + } else { + Ok(BorrowerOutputMetadata::Script(bytes.to_vec())) + } + }) + .transpose()?; + + Ok(PreLockMetadata { + borrower_pub_key, + principal_asset_id, + borrower_output_metadata, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decodes_legacy_metadata() { + let borrower_pub_key = [1u8; 32]; + let principal_asset_id = [2u8; 32]; + let mut bytes = [0u8; PRE_LOCK_METADATA_LEN]; + bytes[..32].copy_from_slice(&borrower_pub_key); + bytes[32..].copy_from_slice(&principal_asset_id); + + let metadata = + decode_pre_lock_metadata(&bytes, None).expect("legacy metadata should decode"); + + assert_eq!(metadata.borrower_pub_key(), borrower_pub_key); + assert_eq!(metadata.principal_asset_id(), principal_asset_id); + assert_eq!(metadata.borrower_output_script_hash(), None); + assert_eq!(metadata.borrower_output_script(), None); + } + + #[test] + fn decodes_hash_only_metadata() { + let borrower_pub_key = [1u8; 32]; + let principal_asset_id = [2u8; 32]; + let borrower_output_script_hash = [3u8; 32]; + let metadata = PreLockMetadata::new_with_hash( + borrower_pub_key, + principal_asset_id, + borrower_output_script_hash, + ); + + let decoded = decode_pre_lock_metadata( + &metadata.encode(), + metadata + .encode_borrower_output_metadata() + .as_ref() + .map(AsRef::as_ref), + ) + .expect("hash-only metadata should round-trip"); + + assert_eq!(decoded, metadata); + assert_eq!(decoded.borrower_output_script(), None); + } + + #[test] + fn decodes_script_metadata() { + let borrower_pub_key = [1u8; 32]; + let principal_asset_id = [2u8; 32]; + let borrower_output_script = vec![0x00, 0x14]; + let metadata = PreLockMetadata::new_with_script( + borrower_pub_key, + principal_asset_id, + borrower_output_script.clone(), + ); + + let decoded = decode_pre_lock_metadata( + &metadata.encode(), + metadata + .encode_borrower_output_metadata() + .as_ref() + .map(AsRef::as_ref), + ) + .expect("script metadata should round-trip"); + + assert_eq!(decoded, metadata); + assert_eq!( + decoded.borrower_output_script(), + Some(borrower_output_script.as_slice()) + ); + } +} diff --git a/crates/contracts/src/sdk/pre_lock/mod.rs b/crates/contracts/src/sdk/pre_lock/mod.rs index 6d4ecc8..26af01f 100644 --- a/crates/contracts/src/sdk/pre_lock/mod.rs +++ b/crates/contracts/src/sdk/pre_lock/mod.rs @@ -2,6 +2,7 @@ pub mod cancellation; pub mod creation; pub mod extract_arguments_from_tx; pub mod lending_creation; +pub mod metadata; pub mod utility_nfts_issuance_preparation; pub mod utility_nfts_issuing; @@ -9,5 +10,6 @@ pub use cancellation::*; pub use creation::*; pub use extract_arguments_from_tx::*; pub use lending_creation::*; +pub use metadata::*; pub use utility_nfts_issuance_preparation::*; pub use utility_nfts_issuing::*; diff --git a/crates/contracts/tests/support/wallet_abi_lending_support.rs b/crates/contracts/tests/support/wallet_abi_lending_support.rs index 3e8e3a0..dff35af 100644 --- a/crates/contracts/tests/support/wallet_abi_lending_support.rs +++ b/crates/contracts/tests/support/wallet_abi_lending_support.rs @@ -862,11 +862,17 @@ impl LendingScenario { let mut op_return_data = [0u8; 64]; op_return_data[..32] .copy_from_slice(&self.harness.signer_x_only_public_key_25()?.serialize()); - op_return_data[32..].copy_from_slice(&self.principal_asset_id.into_inner().0); + op_return_data[32..64].copy_from_slice(&self.principal_asset_id.into_inner().0); let metadata_script = { let bytes = el25::encode::serialize(&el25::Script::new_op_return(&op_return_data)); el26::encode::deserialize(&bytes)? }; + let borrower_output_script_metadata_script = { + let bytes = el25::encode::serialize(&el25::Script::new_op_return( + self.harness.wallet_script_25().as_bytes(), + )); + el26::encode::deserialize(&bytes)? + }; let fee_utxo = self.fund_explicit_policy_fee("pre-lock-fee").await?; let pre_lock_creation_finalizer = FinalizerSpec::Simf { source_simf: PRE_LOCK_SOURCE.to_string(), @@ -1021,6 +1027,17 @@ impl LendingScenario { }, blinder: BlinderVariant::Explicit, }, + OutputSchema { + id: "pre-lock-borrower-output-script-hash".into(), + amount_sat: 0, + lock: LockVariant::Script { + script: borrower_output_script_metadata_script, + }, + asset: AssetVariant::AssetId { + asset_id: el26::AssetId::default(), + }, + blinder: BlinderVariant::Explicit, + }, ], )) .await?; diff --git a/crates/indexer/Cargo.toml b/crates/indexer/Cargo.toml index 11fc8a5..bec87e9 100644 --- a/crates/indexer/Cargo.toml +++ b/crates/indexer/Cargo.toml @@ -25,7 +25,7 @@ tracing = "0.1" tracing-log = "0.2" tracing-bunyan-formatter = "0.3" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tower-http = { version = "0.5", features = ["trace", "request-id"] } +tower-http = { version = "0.5", features = ["cors", "trace", "request-id"] } uuid = { version= "1", features = ["v4", "serde"] } config = { version = "0.15.16", default-features = true } tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] } diff --git a/crates/indexer/README.md b/crates/indexer/README.md index 32269bd..458e416 100644 --- a/crates/indexer/README.md +++ b/crates/indexer/README.md @@ -115,6 +115,7 @@ To start the Indexer: ```bash RUN_MODE=indexer cargo run -p lending-indexer ``` +This starts only the blockchain indexing worker. It does not expose the HTTP API and does not listen on port `8000`. To start the API Service: ```bash @@ -126,7 +127,7 @@ cargo run -p lending-indexer > [!TIP] > For readable, pretty-printed logs in your console, pipe the output to bunyan. If you don't have it installed, run `cargo install bunyan`: > ```bash -> RUN_MODE=indexer cargo run -p lending-indexer | bunyan +> RUN_MODE=api cargo run -p lending-indexer | bunyan > ``` ## Development & Testing diff --git a/crates/indexer/configuration/base.yaml b/crates/indexer/configuration/base.yaml index 45e4b21..8c9206c 100644 --- a/crates/indexer/configuration/base.yaml +++ b/crates/indexer/configuration/base.yaml @@ -11,4 +11,4 @@ esplora: timeout: 10 indexer: interval: 10000 - last_indexed_height: 2309541 \ No newline at end of file + last_indexed_height: 2342450 \ No newline at end of file diff --git a/crates/indexer/src/api/server.rs b/crates/indexer/src/api/server.rs index 19fbaee..9af3bab 100644 --- a/crates/indexer/src/api/server.rs +++ b/crates/indexer/src/api/server.rs @@ -1,11 +1,14 @@ -use std::sync::Arc; +use std::{io, sync::Arc}; use axum::{ Router, + http::Method, routing::{get, post}, }; use sqlx::PgPool; use tokio::net::TcpListener; +use tokio::task::JoinSet; +use tower_http::cors::{Any, CorsLayer}; use tower_http::request_id::{self, MakeRequestUuid, RequestId}; use tower_http::trace::TraceLayer; @@ -19,10 +22,13 @@ pub struct AppState { pub db: PgPool, } -pub async fn run_server(listener: TcpListener, db_pool: PgPool) { - let state = Arc::new(AppState { db: db_pool }); +fn build_app(state: Arc) -> Router { + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_headers(Any); - let app = Router::new() + Router::new() .route("/offers", get(get_offers_short_info)) .route("/offers/full", get(get_offers_full_info)) .route("/offers/batch", post(get_offer_details_batch)) @@ -42,6 +48,7 @@ pub async fn run_server(listener: TcpListener, db_pool: PgPool) { ) .route("/offers/{id}/utxos", get(get_offer_utxos_history)) .with_state(state) + .layer(cors) .layer( TraceLayer::new_for_http().make_span_with(|request: &axum::http::Request<_>| { let request_id = request @@ -59,7 +66,100 @@ pub async fn run_server(listener: TcpListener, db_pool: PgPool) { }), ) .layer(request_id::PropagateRequestIdLayer::x_request_id()) - .layer(request_id::SetRequestIdLayer::x_request_id(MakeRequestUuid)); + .layer(request_id::SetRequestIdLayer::x_request_id(MakeRequestUuid)) +} + +fn format_bind_target(host: &str, port: u16) -> String { + if host.contains(':') && !host.starts_with('[') { + format!("[{host}]:{port}") + } else { + format!("{host}:{port}") + } +} + +pub fn listener_bind_targets(host: &str, port: u16) -> Vec { + match host { + "127.0.0.1" | "localhost" => vec![ + format_bind_target("127.0.0.1", port), + format_bind_target("::1", port), + ], + "0.0.0.0" => vec![ + format_bind_target("0.0.0.0", port), + format_bind_target("::", port), + ], + _ => vec![format_bind_target(host, port)], + } +} + +pub async fn bind_listeners(host: &str, port: u16) -> io::Result> { + let mut listeners = Vec::new(); + let mut first_error = None; + + for bind_target in listener_bind_targets(host, port) { + match TcpListener::bind(&bind_target).await { + Ok(listener) => listeners.push(listener), + Err(error) => { + if listeners.is_empty() && first_error.is_none() { + first_error = Some(io::Error::new( + error.kind(), + format!("failed to bind {bind_target}: {error}"), + )); + } else { + tracing::warn!(%bind_target, %error, "Failed to bind API listener"); + } + } + } + } + + if listeners.is_empty() { + return Err(first_error.unwrap_or_else(|| io::Error::other("failed to bind API listener"))); + } + + Ok(listeners) +} + +pub async fn run_server(listeners: Vec, db_pool: PgPool) -> io::Result<()> { + let state = Arc::new(AppState { db: db_pool }); + let app = build_app(state); + let mut servers = JoinSet::new(); + + for listener in listeners { + let app = app.clone(); + servers.spawn(async move { axum::serve(listener, app).await }); + } + + match servers.join_next().await { + Some(Ok(result)) => result, + Some(Err(error)) => Err(io::Error::other(format!("API server task failed: {error}"))), + None => Ok(()), + } +} + +#[cfg(test)] +mod tests { + use super::listener_bind_targets; + + #[test] + fn localhost_binds_ipv4_and_ipv6_loopback() { + assert_eq!( + listener_bind_targets("localhost", 8000), + vec!["127.0.0.1:8000", "[::1]:8000"] + ); + } + + #[test] + fn ipv4_any_binds_dual_stack_targets() { + assert_eq!( + listener_bind_targets("0.0.0.0", 8000), + vec!["0.0.0.0:8000", "[::]:8000"] + ); + } - axum::serve(listener, app).await.unwrap() + #[test] + fn custom_host_is_preserved() { + assert_eq!( + listener_bind_targets("192.168.1.50", 9000), + vec!["192.168.1.50:9000"] + ); + } } diff --git a/crates/indexer/src/main.rs b/crates/indexer/src/main.rs index a5bbed3..eae99c2 100644 --- a/crates/indexer/src/main.rs +++ b/crates/indexer/src/main.rs @@ -2,7 +2,6 @@ use lending_indexer::esplora_client::EsploraClient; use lending_indexer::telemetry::{get_subscriber, init_subscriber}; use lending_indexer::{api, indexer}; use sqlx::PgPool; -use tokio::net::TcpListener; use lending_indexer::configuration::get_configuration; @@ -21,18 +20,24 @@ async fn main() -> Result<(), std::io::Error> { "indexer" => { let esplora_client = EsploraClient::with_base_url(&configuration.esplora.base_url); - tracing::info!("Starting indexer service"); + tracing::info!("Starting indexer service (background worker only; no HTTP listener)"); indexer::worker::run_indexer(configuration.indexer, pool, esplora_client).await; } _ => { - let address = format!( - "{}:{}", - configuration.application.host, configuration.application.port - ); - let listener = TcpListener::bind(address).await?; - - tracing::info!("Starting api server"); - api::server::run_server(listener, pool).await; + let listeners = api::server::bind_listeners( + &configuration.application.host, + configuration.application.port, + ) + .await?; + + let listen_addresses = listeners + .iter() + .filter_map(|listener| listener.local_addr().ok()) + .map(|addr| addr.to_string()) + .collect::>(); + + tracing::info!(?listen_addresses, "Starting api server"); + api::server::run_server(listeners, pool).await?; } } diff --git a/web/.env.example b/web/.env.example index dcdb3ad..f522824 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,6 +1,12 @@ # Base URL of the lending indexer API (default: http://localhost:8000) VITE_API_URL=http://localhost:8000 +# Reown project id for the official WalletConnect transport +VITE_REOWN_PROJECT_ID=your_reown_project_id + +# Wallet ABI network: liquid | testnet-liquid | localtest-liquid (default: testnet-liquid) +VITE_WALLET_ABI_NETWORK=testnet-liquid + # Esplora API base URL for chain data (default: Liquid Testnet) VITE_ESPLORA_BASE_URL=https://blockstream.info/liquidtestnet/api diff --git a/web/README.md b/web/README.md index 46a8339..04614fa 100644 --- a/web/README.md +++ b/web/README.md @@ -1,15 +1,25 @@ # Simplicity Lending — Web UI -Demo frontend for the Simplicity Lending protocol. Uses the [Indexer](../crates/indexer/README.md) REST API. See [repo root](../README.md) for full quick start. +WalletConnect frontend for the Simplicity Lending protocol. The browser no longer derives keys or holds a seed. It connects to a wallet through the Wallet ABI namespace, uses the indexer for public protocol state, and only shows balances that are public from chain/indexer data. ## Prerequisites - Node.js 18+ - [Indexer API](../crates/indexer/README.md) running (API mode, port 8000). The app uses `VITE_API_URL` (default `http://localhost:8000`); see `.env.example`. +- A [Reown](https://reown.com/) project id configured through `VITE_REOWN_PROJECT_ID`. > [!NOTE] > `lwk_web` is a local file dependency. To build it: clone [Blockstream/lwk](https://github.com/Blockstream/lwk), then from the LWK repo run `cd lwk_wasm && RUSTFLAGS='--cfg web_sys_unstable_apis' wasm-pack build --target web --out-dir pkg_web --features simplicity,serial`. Update the `lwk_web` path in `package.json` to point to your `pkg_web` output directory. +## Environment + +Copy `web/.env.example` into a local `.env` and set: + +- `VITE_API_URL` for the lending indexer +- `VITE_REOWN_PROJECT_ID` for the official WalletConnect transport +- `VITE_WALLET_ABI_NETWORK` for `liquid`, `testnet-liquid`, or `localtest-liquid` +- `VITE_ESPLORA_BASE_URL` and optionally `VITE_ESPLORA_EXPLORER_URL` for chain lookups and explorer links + ## Setup ```bash @@ -18,13 +28,27 @@ npm install ## Run -Start the Indexer API (see [crates/indexer/README.md](../crates/indexer/README.md); run from `crates/indexer` or repo root as documented there), then: +Start the indexer, then: ```bash npm run dev ``` -Open the URL shown (e.g. http://localhost:5173). +Open the URL shown (for example `http://localhost:5173`), connect the wallet, and approve the WalletConnect pairing in the wallet app. Existing WalletConnect sessions restore automatically on reload, and stale Wallet ABI sessions for this app are disconnected during startup. + +## Supported Flow + +The web app exposes only the protocol surfaces: + +- `Dashboard` +- `Borrower` +- `Lender` + +The app uses only these wallet JSON-RPC methods: + +- `get_signer_receive_address` +- `get_raw_signing_x_only_pubkey` +- `wallet_abi_process_request` ## Scripts diff --git a/web/package-lock.json b/web/package-lock.json index bd01367..d007695 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,9 +8,16 @@ "name": "web", "version": "0.0.0", "dependencies": { - "lwk_web": "file:../../../Blockstream/lwk/lwk_wasm/pkg_web", + "@reown/appkit": "^1.8.19", + "@reown/appkit-common": "^1.8.19", + "@reown/appkit-universal-connector": "^1.8.19", + "@walletconnect/sign-client": "^2.23.7", + "@walletconnect/types": "^2.23.7", + "@walletconnect/utils": "^2.23.7", + "lwk_web": "file:../../lwk/lwk_wasm/pkg_web", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "wallet-abi-sdk-alpha": "file:../../wallet-abi-sdk" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -41,10 +48,42 @@ "license": "MIT OR BSD-2-Clause" }, "../../../Blockstream/lwk/lwk_wasm/pkg_web": { + "name": "lwk_wasm", + "version": "0.15.0", + "extraneous": true, + "license": "MIT OR BSD-2-Clause" + }, + "../../lwk/lwk_wasm/pkg_web": { "name": "lwk_wasm", "version": "0.15.0", "license": "MIT OR BSD-2-Clause" }, + "../../wallet-abi-sdk": { + "name": "wallet-abi-sdk-alpha", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@arethetypeswrong/cli": "^0.18.2", + "@eslint/js": "^9.39.3", + "@types/bun": "^1.3.10", + "eslint": "^9.39.3", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.8.1", + "publint": "^0.3.18", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1", + "zshy": "^0.7.0" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -89,6 +128,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -292,6 +332,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -340,6 +390,67 @@ "node": ">=6.9.0" } }, + "node_modules/@base-org/account": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@base-org/account/-/account-2.4.0.tgz", + "integrity": "sha512-A4Umpi8B9/pqR78D1Yoze4xHyQaujioVRqqO3d6xuDFw9VRtjg6tK3bPlwE0aW+nVH/ntllCpPa2PbI8Rnjcug==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@coinbase/cdp-sdk": "^1.0.0", + "@noble/hashes": "1.4.0", + "clsx": "1.2.1", + "eventemitter3": "5.0.1", + "idb-keyval": "6.2.1", + "ox": "0.6.9", + "preact": "10.24.2", + "viem": "^2.31.7", + "zustand": "5.0.3" + } + }, + "node_modules/@coinbase/cdp-sdk": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/@coinbase/cdp-sdk/-/cdp-sdk-1.45.0.tgz", + "integrity": "sha512-4fgGOhyN9g/pTDE9NtsKUapwFsubrk9wafz8ltmBqSwWqLZWfWxXkVmzMYYFAf+qeGf/X9JqJtmvDVaHFlXWlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana-program/system": "^0.10.0", + "@solana-program/token": "^0.9.0", + "@solana/kit": "^5.1.0", + "@solana/web3.js": "^1.98.1", + "abitype": "1.0.6", + "axios": "^1.12.2", + "axios-retry": "^4.5.0", + "jose": "^6.0.8", + "md5": "^2.3.0", + "uncrypto": "^0.1.3", + "viem": "^2.21.26", + "zod": "^3.24.4" + } + }, + "node_modules/@coinbase/cdp-sdk/node_modules/abitype": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", + "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -1041,6 +1152,291 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/react": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.8.tgz", + "integrity": "sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==", + "license": "BSD-3-Clause", + "optional": true, + "peerDependencies": { + "@types/react": "17 || 18 || 19" + } + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@phosphor-icons/webcomponents": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@phosphor-icons/webcomponents/-/webcomponents-2.1.5.tgz", + "integrity": "sha512-JcvQkZxvcX2jK+QCclm8+e8HXqtdFW9xV4/kk2aL9Y3dJA2oQVt+pzbv1orkumz3rfx4K9mn9fDoMr1He1yr7Q==", + "license": "MIT", + "dependencies": { + "lit": "^3" + } + }, + "node_modules/@reown/appkit": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.8.19.tgz", + "integrity": "sha512-wB+xatkRbOy0AY1cZxxtcKzzPk3l3CTFulDbaISLVmZI6ZnQrOFuLnYc285zGsC6DB4d6bmwYUh89zcMLa4PvQ==", + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-pay": "1.8.19", + "@reown/appkit-polyfills": "1.8.19", + "@reown/appkit-scaffold-ui": "1.8.19", + "@reown/appkit-ui": "1.8.19", + "@reown/appkit-utils": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "@walletconnect/universal-provider": "2.23.7", + "bs58": "6.0.0", + "semver": "7.7.2", + "valtio": "2.1.7", + "viem": ">=2.45.0" + }, + "optionalDependencies": { + "@lit/react": "1.0.8" + } + }, + "node_modules/@reown/appkit-common": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.8.19.tgz", + "integrity": "sha512-z5wDrYjUGY7YbM4b14NHVo54WKZ5++PQtGkcsXhiOP39yAVijubBQD8BfHs/Pu2fSFqnqLIFoCVvIEfNWWccRw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "big.js": "6.2.2", + "dayjs": "1.11.13", + "viem": ">=2.45.0" + } + }, + "node_modules/@reown/appkit-controllers": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.8.19.tgz", + "integrity": "sha512-JFNT8CfAVit9FJXh596Ye4U8A/oIapW+Y0KQqjB59DXyTCDZbxZDB32rULBQrSkZ6PufTEa239Dil4kABCQKtg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "@walletconnect/universal-provider": "2.23.7", + "valtio": "2.1.7", + "viem": ">=2.45.0" + } + }, + "node_modules/@reown/appkit-pay": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.8.19.tgz", + "integrity": "sha512-HO/tQT0TbTQO3eONxNNPJAOZAOzUiHvjM0Mty1rFFeRBH68auiqQxQi2YFNMs014gNkRN+cb84VYau7+MCC0fQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-ui": "1.8.19", + "@reown/appkit-utils": "1.8.19", + "lit": "3.3.0", + "valtio": "2.1.7" + } + }, + "node_modules/@reown/appkit-polyfills": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.8.19.tgz", + "integrity": "sha512-PSoetRSuZg7f2YFPzdfs4BayQl51zcGqYr7frwOe6td0XEsspLrrVFn/zk5QFbFHZVsMdfRZ+TTunt84ozRdnQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "buffer": "6.0.3" + } + }, + "node_modules/@reown/appkit-scaffold-ui": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.8.19.tgz", + "integrity": "sha512-Ak767x0VzeDIXb0wbzkl19kx6udw7vkb1EU0SAweG3iKc9BunW87Rfcd48/YimzMZycJaYmlbtfmqQQDYs6Few==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-pay": "1.8.19", + "@reown/appkit-ui": "1.8.19", + "@reown/appkit-utils": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "lit": "3.3.0" + } + }, + "node_modules/@reown/appkit-ui": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.8.19.tgz", + "integrity": "sha512-fCAwW8yyyC3JcgKLBPvCtYuDGC4H8anO7u4LTaAXGEzdcU5H+IrCgNFSPNK7NuTSmgXm1TnoYxPxRFKNiNwFdA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@phosphor-icons/webcomponents": "2.1.5", + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "lit": "3.3.0", + "qrcode": "1.5.3" + } + }, + "node_modules/@reown/appkit-ui/node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@reown/appkit-universal-connector": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-universal-connector/-/appkit-universal-connector-1.8.19.tgz", + "integrity": "sha512-9H9t+OkEu7jzZY5FbD8YJM29zUVPE3Xp+PNJgEIv7zcx/9JdDtK+99FvygMZmF/pm5E7Vm3oiyCCuNCkc+3BbA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit": "1.8.19", + "@reown/appkit-common": "1.8.19", + "@walletconnect/types": "2.23.7", + "@walletconnect/universal-provider": "2.23.7", + "bs58": "6.0.0" + } + }, + "node_modules/@reown/appkit-utils": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.8.19.tgz", + "integrity": "sha512-VQPgUMTFqoh4UD3EDZSw9wyMkyZsmIVmu8CdQ2FUxIuqYW4fLd0VIpkDeO64MMhSv8b0X8Vd6m4+eGcqSwlUAg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-polyfills": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "@wallet-standard/wallet": "1.1.0", + "@walletconnect/logger": "3.0.2", + "@walletconnect/universal-provider": "2.23.7", + "valtio": "2.1.7", + "viem": ">=2.45.0" + }, + "optionalDependencies": { + "@base-org/account": "2.4.0", + "@safe-global/safe-apps-provider": "0.18.6", + "@safe-global/safe-apps-sdk": "9.1.0" + }, + "peerDependencies": { + "valtio": "2.1.7" + } + }, + "node_modules/@reown/appkit-wallet": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.8.19.tgz", + "integrity": "sha512-NVdIKceUhkXYtsG32925ctmVn0QJFNyDlr+mWheMLCEZ/IUPn+6aA53vTVaSUquhyeFxUXtrCOh3ln6v1tup5w==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-polyfills": "1.8.19", + "@walletconnect/logger": "3.0.2", + "zod": "3.22.4" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@reown/appkit/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1398,129 +1794,1319 @@ "win32" ] }, - "node_modules/@tailwindcss/node": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz", - "integrity": "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==", - "dev": true, + "node_modules/@safe-global/safe-apps-provider": { + "version": "0.18.6", + "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.6.tgz", + "integrity": "sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==", "license": "MIT", + "optional": true, "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.31.1", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.0" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.0.tgz", - "integrity": "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.0", - "@tailwindcss/oxide-darwin-arm64": "4.2.0", - "@tailwindcss/oxide-darwin-x64": "4.2.0", - "@tailwindcss/oxide-freebsd-x64": "4.2.0", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.0", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.0", - "@tailwindcss/oxide-linux-x64-musl": "4.2.0", - "@tailwindcss/oxide-wasm32-wasi": "4.2.0", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.0" + "@safe-global/safe-apps-sdk": "^9.1.0", + "events": "^3.3.0" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz", - "integrity": "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@safe-global/safe-apps-sdk": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.1.0.tgz", + "integrity": "sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q==", "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "@safe-global/safe-gateway-typescript-sdk": "^3.5.3", + "viem": "^2.1.1" } }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz", - "integrity": "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@safe-global/safe-gateway-typescript-sdk": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.23.1.tgz", + "integrity": "sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==", "license": "MIT", "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">= 20" + "node": ">=16" } }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz", - "integrity": "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz", - "integrity": "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz", - "integrity": "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 20" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@solana-program/system": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@solana-program/system/-/system-0.10.0.tgz", + "integrity": "sha512-Go+LOEZmqmNlfr+Gjy5ZWAdY5HbYzk2RBewD9QinEU/bBSzpFfzqDRT55JjFRBGJUvMgf3C2vfXEGT4i8DSI4g==", + "license": "Apache-2.0", + "optional": true, + "peerDependencies": { + "@solana/kit": "^5.0" + } + }, + "node_modules/@solana-program/token": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@solana-program/token/-/token-0.9.0.tgz", + "integrity": "sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA==", + "license": "Apache-2.0", + "optional": true, + "peerDependencies": { + "@solana/kit": "^5.0" + } + }, + "node_modules/@solana/accounts": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/accounts/-/accounts-5.5.1.tgz", + "integrity": "sha512-TfOY9xixg5rizABuLVuZ9XI2x2tmWUC/OoN556xwfDlhBHBjKfszicYYOyD6nbFmwTGYarCmyGIdteXxTXIdhQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/rpc-spec": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/addresses": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/addresses/-/addresses-5.5.1.tgz", + "integrity": "sha512-5xoah3Q9G30HQghu/9BiHLb5pzlPKRC3zydQDmE3O9H//WfayxTFppsUDCL6FjYUHqj/wzK6CWHySglc2RkpdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/assertions": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/nominal-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/assertions": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/assertions/-/assertions-5.5.1.tgz", + "integrity": "sha512-YTCSWAlGwSlVPnWtWLm3ukz81wH4j2YaCveK+TjpvUU88hTy6fmUqxi0+hvAMAe4zKXpJyj3Az7BrLJRxbIm4Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-5.5.1.tgz", + "integrity": "sha512-Vea29nJub/bXjfzEV7ZZQ/PWr1pYLZo3z0qW0LQL37uKKVzVFRQlwetd7INk3YtTD3xm9WUYr7bCvYUk3uKy2g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/options": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-core": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-5.5.1.tgz", + "integrity": "sha512-TgBt//bbKBct0t6/MpA8ElaOA3sa8eYVvR7LGslCZ84WiAwwjCY0lW/lOYsFHJQzwREMdUyuEyy5YWBKtdh8Rw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-data-structures": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-5.5.1.tgz", + "integrity": "sha512-97bJWGyUY9WvBz3mX1UV3YPWGDTez6btCfD0ip3UVEXJbItVuUiOkzcO5iFDUtQT5riKT6xC+Mzl+0nO76gd0w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-5.5.1.tgz", + "integrity": "sha512-rllMIZAHqmtvC0HO/dc/21wDuWaD0B8Ryv8o+YtsICQBuiL/0U4AGwH7Pi5GNFySYk0/crSuwfIqQFtmxNSPFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-strings": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-5.5.1.tgz", + "integrity": "sha512-7klX4AhfHYA+uKKC/nxRGP2MntbYQCR3N6+v7bk1W/rSxYuhNmt+FN8aoThSZtWIKwN6BEyR1167ka8Co1+E7A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "fastestsmallesttextencoderdecoder": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/errors": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-5.5.1.tgz", + "integrity": "sha512-vFO3p+S7HoyyrcAectnXbdsMfwUzY2zYFUc2DEe5BwpiE9J1IAxPBGjOWO6hL1bbYdBrlmjNx8DXCslqS+Kcmg==", + "license": "MIT", + "optional": true, + "dependencies": { + "chalk": "5.6.2", + "commander": "14.0.2" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/errors/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@solana/fast-stable-stringify": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/fast-stable-stringify/-/fast-stable-stringify-5.5.1.tgz", + "integrity": "sha512-Ni7s2FN33zTzhTFgRjEbOVFO+UAmK8qi3Iu0/GRFYK4jN696OjKHnboSQH/EacQ+yGqS54bfxf409wU5dsLLCw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/functional": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/functional/-/functional-5.5.1.tgz", + "integrity": "sha512-tTHoJcEQq3gQx5qsdsDJ0LEJeFzwNpXD80xApW9o/PPoCNimI3SALkZl+zNW8VnxRrV3l3yYvfHWBKe/X3WG3w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/instruction-plans": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/instruction-plans/-/instruction-plans-5.5.1.tgz", + "integrity": "sha512-7z3CB7YMcFKuVvgcnNY8bY6IsZ8LG61Iytbz7HpNVGX2u1RthOs1tRW8luTzSG1MPL0Ox7afyAVMYeFqSPHnaQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/instructions": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/promises": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/instructions": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/instructions/-/instructions-5.5.1.tgz", + "integrity": "sha512-h0G1CG6S+gUUSt0eo6rOtsaXRBwCq1+Js2a+Ps9Bzk9q7YHNFA75/X0NWugWLgC92waRp66hrjMTiYYnLBoWOQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/keys": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/keys/-/keys-5.5.1.tgz", + "integrity": "sha512-KRD61cL7CRL+b4r/eB9dEoVxIf/2EJ1Pm1DmRYhtSUAJD2dJ5Xw8QFuehobOGm9URqQ7gaQl+Fkc1qvDlsWqKg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/assertions": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/nominal-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/nominal-types": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/nominal-types/-/nominal-types-5.5.1.tgz", + "integrity": "sha512-I1ImR+kfrLFxN5z22UDiTWLdRZeKtU0J/pkWkO8qm/8WxveiwdIv4hooi8pb6JnlR4mSrWhq0pCIOxDYrL9GIQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/offchain-messages": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/offchain-messages/-/offchain-messages-5.5.1.tgz", + "integrity": "sha512-g+xHH95prTU+KujtbOzj8wn+C7ZNoiLhf3hj6nYq3MTyxOXtBEysguc97jJveUZG0K97aIKG6xVUlMutg5yxhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/nominal-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/options": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-5.5.1.tgz", + "integrity": "sha512-eo971c9iLNLmk+yOFyo7yKIJzJ/zou6uKpy6mBuyb/thKtS/haiKIc3VLhyTXty3OH2PW8yOlORJnv4DexJB8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/plugin-core": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/plugin-core/-/plugin-core-5.5.1.tgz", + "integrity": "sha512-VUZl30lDQFJeiSyNfzU1EjYt2QZvoBFKEwjn1lilUJw7KgqD5z7mbV7diJhT+dLFs36i0OsjXvq5kSygn8YJ3A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/programs": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/programs/-/programs-5.5.1.tgz", + "integrity": "sha512-7U9kn0Jsx1NuBLn5HRTFYh78MV4XN145Yc3WP/q5BlqAVNlMoU9coG5IUTJIG847TUqC1lRto3Dnpwm6T4YRpA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/promises": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/promises/-/promises-5.5.1.tgz", + "integrity": "sha512-T9lfuUYkGykJmppEcssNiCf6yiYQxJkhiLPP+pyAc2z84/7r3UVIb2tNJk4A9sucS66pzJnVHZKcZVGUUp6wzA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc/-/rpc-5.5.1.tgz", + "integrity": "sha512-ku8zTUMrkCWci66PRIBC+1mXepEnZH/q1f3ck0kJZ95a06bOTl5KU7HeXWtskkyefzARJ5zvCs54AD5nxjQJ+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/fast-stable-stringify": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/rpc-api": "5.5.1", + "@solana/rpc-spec": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "@solana/rpc-transformers": "5.5.1", + "@solana/rpc-transport-http": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-api": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-api/-/rpc-api-5.5.1.tgz", + "integrity": "sha512-XWOQQPhKl06Vj0xi3RYHAc6oEQd8B82okYJ04K7N0Vvy3J4PN2cxeK7klwkjgavdcN9EVkYCChm2ADAtnztKnA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/rpc-parsed-types": "5.5.1", + "@solana/rpc-spec": "5.5.1", + "@solana/rpc-transformers": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-parsed-types": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-parsed-types/-/rpc-parsed-types-5.5.1.tgz", + "integrity": "sha512-HEi3G2nZqGEsa3vX6U0FrXLaqnUCg4SKIUrOe8CezD+cSFbRTOn3rCLrUmJrhVyXlHoQVaRO9mmeovk31jWxJg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-spec": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-spec/-/rpc-spec-5.5.1.tgz", + "integrity": "sha512-m3LX2bChm3E3by4mQrH4YwCAFY57QBzuUSWqlUw7ChuZ+oLLOq7b2czi4i6L4Vna67j3eCmB3e+4tqy1j5wy7Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/rpc-spec-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-spec-types": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-spec-types/-/rpc-spec-types-5.5.1.tgz", + "integrity": "sha512-6OFKtRpIEJQs8Jb2C4OO8KyP2h2Hy1MFhatMAoXA+0Ik8S3H+CicIuMZvGZ91mIu/tXicuOOsNNLu3HAkrakrw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions/-/rpc-subscriptions-5.5.1.tgz", + "integrity": "sha512-CTMy5bt/6mDh4tc6vUJms9EcuZj3xvK0/xq8IQ90rhkpYvate91RjBP+egvjgSayUg9yucU9vNuUpEjz4spM7w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/fast-stable-stringify": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/promises": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "@solana/rpc-subscriptions-api": "5.5.1", + "@solana/rpc-subscriptions-channel-websocket": "5.5.1", + "@solana/rpc-subscriptions-spec": "5.5.1", + "@solana/rpc-transformers": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/subscribable": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-api": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-api/-/rpc-subscriptions-api-5.5.1.tgz", + "integrity": "sha512-5Oi7k+GdeS8xR2ly1iuSFkAv6CZqwG0Z6b1QZKbEgxadE1XGSDrhM2cn59l+bqCozUWCqh4c/A2znU/qQjROlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/rpc-subscriptions-spec": "5.5.1", + "@solana/rpc-transformers": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-channel-websocket": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-channel-websocket/-/rpc-subscriptions-channel-websocket-5.5.1.tgz", + "integrity": "sha512-7tGfBBrYY8TrngOyxSHoCU5shy86iA9SRMRrPSyBhEaZRAk6dnbdpmUTez7gtdVo0BCvh9nzQtUycKWSS7PnFQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/rpc-subscriptions-spec": "5.5.1", + "@solana/subscribable": "5.5.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-spec": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-spec/-/rpc-subscriptions-spec-5.5.1.tgz", + "integrity": "sha512-iq+rGq5fMKP3/mKHPNB6MC8IbVW41KGZg83Us/+LE3AWOTWV1WT20KT2iH1F1ik9roi42COv/TpoZZvhKj45XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/promises": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "@solana/subscribable": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-transformers": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-transformers/-/rpc-transformers-5.5.1.tgz", + "integrity": "sha512-OsWqLCQdcrRJKvHiMmwFhp9noNZ4FARuMkHT5us3ustDLXaxOjF0gfqZLnMkulSLcKt7TGXqMhBV+HCo7z5M8Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/nominal-types": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-transport-http": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-transport-http/-/rpc-transport-http-5.5.1.tgz", + "integrity": "sha512-yv8GoVSHqEV0kUJEIhkdOVkR2SvJ6yoWC51cJn2rSV7plr6huLGe0JgujCmB7uZhhaLbcbP3zxXxu9sOjsi7Fg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/rpc-spec": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "undici-types": "^7.19.2" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-transport-http/node_modules/undici-types": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.22.0.tgz", + "integrity": "sha512-RKZvifiL60xdsIuC80UY0dq8Z7DbJUV8/l2hOVbyZAxBzEeQU4Z58+4ZzJ6WN2Lidi9KzT5EbiGX+PI/UGYuRw==", + "license": "MIT", + "optional": true + }, + "node_modules/@solana/rpc-types": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-types/-/rpc-types-5.5.1.tgz", + "integrity": "sha512-bibTFQ7PbHJJjGJPmfYC2I+/5CRFS4O2p9WwbFraX1Keeel+nRrt/NBXIy8veP5AEn2sVJIyJPpWBRpCx1oATA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/nominal-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/signers": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/signers/-/signers-5.5.1.tgz", + "integrity": "sha512-FY0IVaBT2kCAze55vEieR6hag4coqcuJ31Aw3hqRH7mv6sV8oqwuJmUrx+uFwOp1gwd5OEAzlv6N4hOOple4sQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/instructions": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/nominal-types": "5.5.1", + "@solana/offchain-messages": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/subscribable": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/subscribable/-/subscribable-5.5.1.tgz", + "integrity": "sha512-9K0PsynFq0CsmK1CDi5Y2vUIJpCqkgSS5yfDN0eKPgHqEptLEaia09Kaxc90cSZDZU5mKY/zv1NBmB6Aro9zQQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/sysvars": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/sysvars/-/sysvars-5.5.1.tgz", + "integrity": "sha512-k3Quq87Mm+geGUu1GWv6knPk0ALsfY6EKSJGw9xUJDHzY/RkYSBnh0RiOrUhtFm2TDNjOailg8/m0VHmi3reFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/accounts": "5.5.1", + "@solana/codecs": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/transaction-confirmation": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/transaction-confirmation/-/transaction-confirmation-5.5.1.tgz", + "integrity": "sha512-j4mKlYPHEyu+OD7MBt3jRoX4ScFgkhZC6H65on4Fux6LMScgivPJlwnKoZMnsgxFgWds0pl+BYzSiALDsXlYtw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/promises": "5.5.1", + "@solana/rpc": "5.5.1", + "@solana/rpc-subscriptions": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/transaction-messages": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/transaction-messages/-/transaction-messages-5.5.1.tgz", + "integrity": "sha512-aXyhMCEaAp3M/4fP0akwBBQkFPr4pfwoC5CLDq999r/FUwDax2RE/h4Ic7h2Xk+JdcUwsb+rLq85Y52hq84XvQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/instructions": "5.5.1", + "@solana/nominal-types": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/transactions": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/transactions/-/transactions-5.5.1.tgz", + "integrity": "sha512-8hHtDxtqalZ157pnx6p8k10D7J/KY/biLzfgh9R09VNLLY3Fqi7kJvJCr7M2ik3oRll56pxhraAGCC9yIT6eOA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/instructions": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/nominal-types": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/transaction-messages": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@solana/web3.js/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/helpers/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz", + "integrity": "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.0.tgz", + "integrity": "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-x64": "4.2.0", + "@tailwindcss/oxide-freebsd-x64": "4.2.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-x64-musl": "4.2.0", + "@tailwindcss/oxide-wasm32-wasi": "4.2.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz", + "integrity": "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz", + "integrity": "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz", + "integrity": "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz", + "integrity": "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz", + "integrity": "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { @@ -1714,6 +3300,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1732,7 +3328,7 @@ "version": "24.10.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -1742,8 +3338,9 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1758,6 +3355,29 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", @@ -1803,6 +3423,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -2104,47 +3725,474 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", - "dev": true, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@wallet-standard/base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", + "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@wallet-standard/wallet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/wallet/-/wallet-1.1.0.tgz", + "integrity": "sha512-Gt8TnSlDZpAl+RWOOAB/kuvC7RpcdWAlFbHNoi4gsXsfaWa1QCT6LBcfIYTPdOZC9OVZUDwqGuGAcqZejDmHjg==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@walletconnect/core": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.7.tgz", + "integrity": "sha512-yTyymn9mFaDZkUfLfZ3E9VyaSDPeHAXlrPxQRmNx2zFsEt/25GmTU2A848aomimLxZnAG2jNLhxbJ8I0gyNV+w==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.7", + "@walletconnect/utils": "2.23.7", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.44.0", + "events": "3.3.0", + "uint8arrays": "3.1.1" + }, + "engines": { + "node": ">=18.20.8" + } + }, + "node_modules/@walletconnect/environment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", + "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", + "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", + "license": "MIT", + "dependencies": { + "keyvaluestorage-interface": "^1.0.0", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/heartbeat": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", + "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", + "license": "MIT", + "dependencies": { + "@walletconnect/events": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-http-connection": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.8.tgz", + "integrity": "sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.1", + "cross-fetch": "^3.1.4", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-provider": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", + "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.8", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", + "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0", + "keyvaluestorage-interface": "^1.0.0" + } + }, + "node_modules/@walletconnect/jsonrpc-utils": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", + "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", + "license": "MIT", + "dependencies": { + "@walletconnect/environment": "^1.0.1", + "@walletconnect/jsonrpc-types": "^1.0.3", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/jsonrpc-ws-connection": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", + "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0", + "ws": "^7.5.1" + } + }, + "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/logger": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.2.tgz", + "integrity": "sha512-7wR3wAwJTOmX4gbcUZcFMov8fjftY05+5cO/d4cpDD8wDzJ+cIlKdYOXaXfxHLSYeDazMXIsxMYjHYVDfkx+nA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@walletconnect/relay-api": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", + "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-types": "^1.0.2" + } + }, + "node_modules/@walletconnect/relay-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", + "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.0", + "@noble/hashes": "1.7.0", + "@walletconnect/safe-json": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "uint8arrays": "^3.0.0" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/curves": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", + "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/safe-json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", + "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/sign-client": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.7.tgz", + "integrity": "sha512-SX61lzb1bTl/LijlcHQttnoHPBzzoY5mW9ArR6qhFtDNDTS7yr2rcH7rCngxHlYeb4rAYcWLHgbiGSrdKxl/mg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/core": "2.23.7", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "3.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.7", + "@walletconnect/utils": "2.23.7", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", + "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/types": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.7.tgz", + "integrity": "sha512-6PAKK+iR2IntmlkCFLMAHjYeIaerCJJYRDmdRimhon0u+aNmQT+HyGM6zxDAth0rdpBD7qEvKP5IXZTE7KFUhw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/universal-provider": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.23.7.tgz", + "integrity": "sha512-6UicU/Mhr/1bh7MNoajypz7BhigORbHpP1LFTf8FYLQGDqzmqHMqmMH2GDAImtaY2sFTi2jBvc22tLl8VMze/A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/jsonrpc-http-connection": "1.0.8", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/sign-client": "2.23.7", + "@walletconnect/types": "2.23.7", + "@walletconnect/utils": "2.23.7", + "es-toolkit": "1.44.0", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/utils": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.7.tgz", + "integrity": "sha512-3p38gNrkVcIiQixVrlsWSa66Gjs5PqHOug2TxDgYUVBW5NcKjwQA08GkC6CKBQUfr5iaCtbfy6uZJW1LKSIvWQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@msgpack/msgpack": "3.1.3", + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0", + "@scure/base": "1.2.6", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.7", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "blakejs": "1.2.1", + "detect-browser": "5.3.0", + "ox": "0.9.3", + "uint8arrays": "3.1.1" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/ox": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", + "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", - "dev": true, + "node_modules/@walletconnect/window-getters": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", + "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "tslib": "1.14.1" } }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", - "dev": true, + "node_modules/@walletconnect/window-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", + "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, + "@walletconnect/window-getters": "^1.0.1", + "tslib": "1.14.1" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } } }, "node_modules/acorn": { @@ -2153,6 +4201,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2170,6 +4219,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2187,11 +4249,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2203,6 +4273,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2220,6 +4315,22 @@ "node": ">=12" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT", + "optional": true + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.24", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", @@ -2257,6 +4368,19 @@ "postcss": "^8.1.0" } }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2264,6 +4388,32 @@ "dev": true, "license": "MIT" }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -2274,6 +4424,64 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/big.js": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", + "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" + } + }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "license": "MIT", + "optional": true + }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/borsh/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/borsh/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base-x": "^3.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2305,6 +4513,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2319,6 +4528,39 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2329,6 +4571,20 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2339,6 +4595,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001770", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", @@ -2394,6 +4659,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -2404,11 +4679,46 @@ "node": ">= 16" } }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2421,9 +4731,31 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2438,6 +4770,21 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2453,11 +4800,36 @@ "node": ">= 8" } }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "license": "MIT" }, "node_modules/debug": { @@ -2478,21 +4850,71 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", + "optional": true, "engines": { - "node": ">=6" + "node": ">=0.4.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-browser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", + "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", "license": "MIT" }, "node_modules/detect-libc": { @@ -2505,6 +4927,27 @@ "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", @@ -2512,6 +4955,18 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.19.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", @@ -2526,6 +4981,26 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2533,6 +5008,62 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT", + "optional": true + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "es6-promise": "^4.0.3" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2604,6 +5135,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2808,6 +5340,21 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2818,6 +5365,15 @@ "node": ">=12.0.0" } }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "optional": true, + "engines": { + "node": "> 0.1.90" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2839,6 +5395,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT", + "optional": true + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2908,6 +5471,44 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -2937,6 +5538,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2947,6 +5558,54 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "optional": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2973,6 +5632,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2980,6 +5652,23 @@ "dev": true, "license": "ISC" }, + "node_modules/h3": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", + "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.5", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2990,6 +5679,48 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -3007,6 +5738,42 @@ "hermes-estree": "0.25.1" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3044,6 +5811,22 @@ "node": ">=0.8.19" } }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT", + "optional": true + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3054,6 +5837,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3067,6 +5859,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3074,6 +5879,94 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT", + "optional": true + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "optional": true + }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -3084,6 +5977,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", + "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3138,6 +6041,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3161,6 +6071,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/keyvaluestorage-interface": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", + "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3436,6 +6352,37 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lit": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", + "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3477,7 +6424,7 @@ } }, "node_modules/lwk_web": { - "resolved": "../../../Blockstream/lwk/lwk_wasm/pkg_web", + "resolved": "../../lwk/lwk_wasm/pkg_web", "link": true }, "node_modules/magic-string": { @@ -3490,6 +6437,51 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3507,9 +6499,15 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3536,6 +6534,50 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -3543,6 +6585,35 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3561,6 +6632,49 @@ "node": ">= 0.8.0" } }, + "node_modules/ox": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", + "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3593,6 +6707,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3610,7 +6733,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3656,6 +6778,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3663,6 +6786,52 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", + "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "slow-redact": "^0.3.0", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3683,6 +6852,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3699,6 +6869,17 @@ "dev": true, "license": "MIT" }, + "node_modules/preact": { + "version": "10.24.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", + "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3725,6 +6906,35 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", + "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT", + "optional": true + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3735,11 +6945,24 @@ "node": ">=6" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3766,6 +6989,43 @@ "node": ">=0.10.0" } }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3821,6 +7081,84 @@ "fsevents": "~2.3.2" } }, + "node_modules/rpc-websockets": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.5.tgz", + "integrity": "sha512-4mAmr+AEhPYJ9TmDtxF3r3ZcbWy7W8kvZ4PoZYw/Xgp2J7WixjwTgiQZsoTDvch5nimmg3Ay6/0Kuh9oIvVs9A==", + "license": "LGPL-3.0-only", + "optional": true, + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^11.0.0", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^6.0.0" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3837,6 +7175,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3867,6 +7211,21 @@ "dev": true, "license": "ISC" }, + "node_modules/slow-redact": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/slow-redact/-/slow-redact-0.3.2.tgz", + "integrity": "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3877,6 +7236,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3891,6 +7259,49 @@ "dev": true, "license": "MIT" }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3904,6 +7315,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3938,6 +7359,21 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==", + "optional": true + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3999,6 +7435,12 @@ "node": ">=14.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -4012,6 +7454,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4029,8 +7477,9 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4063,13 +7512,139 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/uint8arrays": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", + "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/unstorage": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.4.tgz", + "integrity": "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.5", + "lru-cache": "^11.2.0", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -4111,12 +7686,169 @@ "punycode": "^2.1.0" } }, + "node_modules/utf-8-validate": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/valtio": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.7.tgz", + "integrity": "sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg==", + "license": "MIT", + "dependencies": { + "proxy-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/viem": { + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.0.tgz", + "integrity": "sha512-jU5e1E1s5E5M1y+YrELDnNar/34U8NXfVcRfxtVETigs2gS1vvW2ngnBoQUGBwLnNr0kNv+NUu4m10OqHByoFw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.0", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ox": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.0.tgz", + "integrity": "sha512-WLOB7IKnmI3Ol6RAqY7CJdZKl8QaI44LN91OGF1061YIeN6bL5IsFcdp7+oQShRyamE/8fW/CBRWhJAOzI35Dw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -5228,6 +8960,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -5282,6 +9015,26 @@ } } }, + "node_modules/wallet-abi-sdk-alpha": { + "resolved": "../../wallet-abi-sdk", + "link": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5298,6 +9051,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -5325,6 +9084,48 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -5332,6 +9133,93 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5349,8 +9237,9 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -5367,6 +9256,36 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/web/package.json b/web/package.json index 7eb4c7e..e04141d 100644 --- a/web/package.json +++ b/web/package.json @@ -14,12 +14,18 @@ "test:watch": "vitest" }, "dependencies": { - "lwk_web": "file:../../../Blockstream/lwk/lwk_wasm/pkg_web", + "@reown/appkit": "^1.8.19", + "@reown/appkit-common": "^1.8.19", + "@reown/appkit-universal-connector": "^1.8.19", + "@walletconnect/sign-client": "^2.23.7", + "@walletconnect/types": "^2.23.7", + "@walletconnect/utils": "^2.23.7", + "lwk_web": "file:../../lwk/lwk_wasm/pkg_web", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "wallet-abi-sdk-alpha": "file:../../wallet-abi-sdk" }, "devDependencies": { - "vitest": "^2.1.0", "@eslint/js": "^9.39.1", "@tailwindcss/postcss": "^4.2.0", "@types/node": "^24.10.1", @@ -37,6 +43,7 @@ "tailwindcss": "^4.2.0", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vitest": "^2.1.0" } } diff --git a/web/src/App.tsx b/web/src/App.tsx index 897a245..717cbf5 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,223 +1,178 @@ -import { useState, useEffect } from 'react' -import { SeedGate } from './SeedGate' -import { Utility } from './pages/Utility' -import { CreateOfferPage } from './pages/CreateOffer' +import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent } from 'react' import { Dashboard } from './pages/Dashboard' +import { BorrowerPage } from './pages/Borrower' import { LenderPage } from './pages/Lender' -import { AccountMenu } from './components/AccountMenu' -import { parseSeedHex, deriveSecretKeyFromIndex } from './utility/seed' -import { getP2pkAddressFromSecret } from './utility/addressP2pk' +import { UtilityPage } from './pages/Utility' +import { WalletAbiSessionProvider, useWalletAbiSession } from './walletAbi/WalletAbiSessionContext' +import { WalletConnectCard } from './walletAbi/WalletConnectCard' -const SEED_STORAGE_KEY = 'simplicity-lending-seed-hex' -const ACCOUNT_INDEX_STORAGE_KEY = 'simplicity-lending-account-index' -const P2PK_NETWORK: 'testnet' | 'mainnet' = 'testnet' +export type Tab = 'dashboard' | 'borrower' | 'lender' | 'utility' -export type Tab = 'dashboard' | 'utility' | 'borrower' | 'lender' +function tabFromHash(hash: string): Tab { + const normalized = hash.replace(/^#\/?/, '').trim().toLowerCase() + switch (normalized) { + case 'borrower': + return 'borrower' + case 'lender': + return 'lender' + case 'utility': + return 'utility' + case 'dashboard': + default: + return 'dashboard' + } +} -function Header({ - tab, - onTab, - accountIndex, - accountAddress, - addressLoading, - onAccountIndexChange, - onDisconnect, - showTabs, -}: { - tab: Tab - onTab: (t: Tab) => void - accountIndex: number - accountAddress: string | null - addressLoading: boolean - onAccountIndexChange: (index: number) => void - onDisconnect: () => void - showTabs: boolean -}) { - return ( -
-
-
-

LENDING DEMO

-

POWERED BY SIMPLICITY

-
- {showTabs && ( -
- - -
- )} -
-
- ) +function hashForTab(tab: Tab): string { + return `#/${tab}` } -function AppContent({ - tab, - onTab, - accountIndex, -}: { - tab: Tab - onTab: (t: Tab) => void - accountIndex: number -}) { - return ( -
- {tab === 'dashboard' && } +function shortId(value: string, head = 8, tail = 4): string { + if (value.length <= head + tail) return value + return `${value.slice(0, head)}…${value.slice(-tail)}` +} - {tab === 'utility' && } +function WalletSessionMenu() { + const { signingXOnlyPubkey, network, disconnect } = useWalletAbiSession() + const [open, setOpen] = useState(false) + const ref = useRef(null) - {tab === 'borrower' && } + useEffect(() => { + if (!open) return + const onPointerDown = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', onPointerDown) + return () => document.removeEventListener('mousedown', onPointerDown) + }, [open]) + + if (!signingXOnlyPubkey) return null - {tab === 'lender' && } -
+ return ( +
+ + {open && ( +
+

+ Wallet Session +

+

Network: {network ?? 'unknown'}

+

{signingXOnlyPubkey}

+ +
+ )} +
) } -function readStoredAccountIndex(): number { - if (typeof localStorage === 'undefined') return 0 - const raw = localStorage.getItem(ACCOUNT_INDEX_STORAGE_KEY) - if (raw === null) return 0 - const n = parseInt(raw, 10) - return Number.isNaN(n) || n < 0 ? 0 : n -} - -export default function App() { - const [seedHex, setSeedHexState] = useState(() => - typeof localStorage !== 'undefined' ? localStorage.getItem(SEED_STORAGE_KEY) : null - ) - const [accountIndex, setAccountIndexState] = useState(readStoredAccountIndex) - const [currentAccountAddress, setCurrentAccountAddress] = useState(null) - const [addressLoading, setAddressLoading] = useState(false) - const [tab, setTab] = useState('dashboard') +function AppShell() { + const { status, network, signerScriptPubkeyHex, signingXOnlyPubkey } = useWalletAbiSession() + const [tab, setTab] = useState(() => tabFromHash(window.location.hash)) + const connected = status === 'connected' && network && signerScriptPubkeyHex && signingXOnlyPubkey useEffect(() => { - if (!seedHex) return - let cancelled = false - void (async () => { - setAddressLoading(true) - setCurrentAccountAddress(null) - try { - const seed = parseSeedHex(seedHex) - const secret = deriveSecretKeyFromIndex(seed, accountIndex) - const r = await getP2pkAddressFromSecret(secret, P2PK_NETWORK) - if (!cancelled) setCurrentAccountAddress(r.address) - } catch { - if (!cancelled) setCurrentAccountAddress(null) - } finally { - if (!cancelled) setAddressLoading(false) - } - })() - return () => { - cancelled = true + const onHashChange = () => { + setTab(tabFromHash(window.location.hash)) } - }, [seedHex, accountIndex]) - const setSeedHex = (hex: string | null) => { - setSeedHexState(hex) - if (hex === null) { - setCurrentAccountAddress(null) - setAddressLoading(false) - } - if (typeof localStorage === 'undefined') return - if (hex === null) { - localStorage.removeItem(SEED_STORAGE_KEY) - } else { - localStorage.setItem(SEED_STORAGE_KEY, hex) - } - } + window.addEventListener('hashchange', onHashChange) + onHashChange() + return () => window.removeEventListener('hashchange', onHashChange) + }, []) - const setAccountIndex = (index: number) => { - const n = index >= 0 ? index : 0 - setAccountIndexState(n) - if (typeof localStorage !== 'undefined') { - localStorage.setItem(ACCOUNT_INDEX_STORAGE_KEY, String(n)) + useEffect(() => { + const nextHash = hashForTab(tab) + if (window.location.hash !== nextHash) { + window.history.replaceState(null, '', nextHash) } - } + }, [tab]) - const handleDisconnect = () => { - setSeedHex(null) - } + return ( +
+
+
+
+

+ Simplicity Lending +

+

+ WalletConnect Wallet ABI +

+
+ {connected ? ( +
+ + +
+ ) : null} +
+
- const handleTab = (t: Tab) => { - setTab(t) - } +
+ {!connected ? ( + + ) : tab === 'dashboard' ? ( + + ) : tab === 'borrower' ? ( + + ) : tab === 'utility' ? ( + + ) : ( + + )} +
+
+ ) +} +export default function App() { return ( -
-
-
- - - -
-
+ + + ) } diff --git a/web/src/SeedContext.tsx b/web/src/SeedContext.tsx deleted file mode 100644 index b96eb27..0000000 --- a/web/src/SeedContext.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createContext, useContext } from 'react' - -export interface SeedContextValue { - seedHex: string - accountIndex: number - /** Returns the 32-byte secret key for the current account (derived from seed + accountIndex). */ - getCurrentSecretKey: () => Uint8Array -} - -export const SeedContext = createContext(null) - -export function useSeedHex(): string | null { - const ctx = useContext(SeedContext) - return ctx?.seedHex ?? null -} - -export function useSeedContext(): SeedContextValue | null { - return useContext(SeedContext) -} diff --git a/web/src/SeedGate.tsx b/web/src/SeedGate.tsx deleted file mode 100644 index 0e91b87..0000000 --- a/web/src/SeedGate.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useState } from 'react' -import { SeedContext } from './SeedContext' -import { parseSeedHex, deriveSecretKeyFromIndex } from './utility/seed' -import { Input } from './components/Input' - -type Props = { - seedHex: string | null - setSeedHex: (hex: string) => void - accountIndex: number - children: React.ReactNode -} - -export function SeedGate({ seedHex, setSeedHex, accountIndex, children }: Props) { - const [inputValue, setInputValue] = useState('') - const [error, setError] = useState(null) - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - setError(null) - try { - const trimmed = inputValue.trim().toLowerCase().replace(/^0x/, '') - parseSeedHex(trimmed) - setSeedHex(trimmed) - } catch (err) { - setError(err instanceof Error ? err.message : String(err)) - } - } - - if (seedHex === null) { - return ( -
-
-

Demo signer: enter SEED_HEX (32 bytes, 64 hex chars)

-
- setInputValue(e.target.value)} - className="flex-1 min-w-0 font-mono" - /> - -
- {error &&

{error}

} -

- Only for testing. No real wallet yet. Seed is not persisted. -

-
-
- ) - } - - const seedBytes = parseSeedHex(seedHex) - const getCurrentSecretKey = () => deriveSecretKeyFromIndex(seedBytes, accountIndex) - return ( - - {children} - - ) -} diff --git a/web/src/api/client.ts b/web/src/api/client.ts index bfc27cf..8f2694b 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -77,6 +77,18 @@ export async function fetchOfferIdsByScript(scriptPubkeyHex: string): Promise String(id)) } +export async function fetchOfferIdsByScripts(scriptPubkeysHex: Iterable): Promise { + const scripts = [...new Set(Array.from(scriptPubkeysHex, (script) => script.trim().toLowerCase()))] + .filter((script) => script.length > 0) + + if (scripts.length === 0) { + return [] + } + + const nestedIds = await Promise.all(scripts.map((script) => fetchOfferIdsByScript(script))) + return [...new Set(nestedIds.flat())] +} + /** Fetch offer IDs where the given key is the borrower (e.g. pending offers created by this user). Expects 32-byte hex (64 chars). */ export async function fetchOfferIdsByBorrowerPubkey(borrowerPubkeyHex: string): Promise { const hex = borrowerPubkeyHex.trim().toLowerCase().replace(/^0x/, '') @@ -115,11 +127,28 @@ export function filterOffersByParticipantRole( scriptPubkeyHex: string, role: ParticipantType ): OfferShort[] { - const scriptLower = scriptPubkeyHex.trim().toLowerCase() + return filterOffersByParticipantScripts(offers, [scriptPubkeyHex], role) +} + +export function filterOffersByParticipantScripts( + offers: OfferWithParticipants[], + scriptPubkeysHex: Iterable, + role: ParticipantType +): OfferShort[] { + const scripts = new Set( + Array.from(scriptPubkeysHex, (script) => script.trim().toLowerCase()).filter( + (script) => script.length > 0 + ) + ) + + if (scripts.size === 0) { + return [] + } + return offers .filter((o) => o.participants.some( - (p) => p.participant_type === role && p.script_pubkey.trim().toLowerCase() === scriptLower + (p) => p.participant_type === role && scripts.has(p.script_pubkey.trim().toLowerCase()) ) ) .map( diff --git a/web/src/api/esplora.ts b/web/src/api/esplora.ts index dc8ee7b..0cae137 100644 --- a/web/src/api/esplora.ts +++ b/web/src/api/esplora.ts @@ -1,7 +1,7 @@ /** * Esplora HTTP API client for chain data. * Mirrors the indexer's EsploraClient (crates/indexer/src/esplora_client.rs). - * Base URL from env VITE_ESPLORA_BASE_URL (required). + * Base URL from env VITE_ESPLORA_BASE_URL. * Explorer URL for tx links from VITE_ESPLORA_EXPLORER_URL (optional; falls back to API base URL). * Results use default JS types (string, number, string[], Uint8Array). */ @@ -9,12 +9,16 @@ /** Default request timeout in milliseconds. */ export const DEFAULT_TIMEOUT_MS = 30_000 -function getBaseUrl(): string { - const env = import.meta.env.VITE_ESPLORA_BASE_URL - if (typeof env !== 'string' || !env.trim()) { - throw new EsploraApiError('VITE_ESPLORA_BASE_URL is not set') +function normalizeBaseUrl(value?: string): string | undefined { + if (typeof value !== 'string' || !value.trim()) { + return undefined } - return env.trim().replace(/\/+$/, '') + return value.trim().replace(/\/+$/, '') +} + +function getBaseUrl(): string | undefined { + const env = import.meta.env.VITE_ESPLORA_BASE_URL + return normalizeBaseUrl(env) } function getExplorerBaseUrl(apiBaseUrl: string): string { @@ -40,18 +44,29 @@ export class EsploraApiError extends Error { * Esplora API client. Create once with optional baseUrl/timeout, then call methods. */ export class EsploraClient { - private readonly baseUrl: string - private readonly explorerBaseUrl: string + private readonly baseUrl: string | undefined + private readonly explorerBaseUrl: string | undefined private readonly timeoutMs: number constructor(baseUrl?: string, timeoutMs: number = DEFAULT_TIMEOUT_MS) { - this.baseUrl = (baseUrl?.trim() && baseUrl.trim().replace(/\/+$/, '')) || getBaseUrl() - this.explorerBaseUrl = getExplorerBaseUrl(this.baseUrl) + this.baseUrl = normalizeBaseUrl(baseUrl) || getBaseUrl() + this.explorerBaseUrl = this.baseUrl ? getExplorerBaseUrl(this.baseUrl) : undefined this.timeoutMs = timeoutMs } + private requireBaseUrl(): string { + if (!this.baseUrl) { + throw new EsploraApiError('VITE_ESPLORA_BASE_URL is not set') + } + return this.baseUrl + } + + private requireExplorerBaseUrl(): string { + return this.explorerBaseUrl ?? this.requireBaseUrl() + } + private async get(path: string): Promise { - const url = `${this.baseUrl}${path}` + const url = `${this.requireBaseUrl()}${path}` const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs) try { @@ -71,7 +86,7 @@ export class EsploraClient { } private async getBytes(path: string): Promise { - const url = `${this.baseUrl}${path}` + const url = `${this.requireBaseUrl()}${path}` const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs) try { @@ -92,7 +107,7 @@ export class EsploraClient { } private async post(path: string, body: string): Promise { - const url = `${this.baseUrl}${path}` + const url = `${this.requireBaseUrl()}${path}` const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs) try { @@ -116,7 +131,7 @@ export class EsploraClient { } } - /** POST /tx — broadcast raw transaction (hex). Returns txid on success. */ + /** POST /tx - broadcast raw transaction (hex). Returns txid on success. */ async broadcastTx(txHex: string): Promise { const body = await this.post('/tx', txHex.trim()) return body.trim() @@ -124,12 +139,12 @@ export class EsploraClient { /** URL of the transaction page on the block explorer (e.g. to open in a new tab). Uses VITE_ESPLORA_EXPLORER_URL if set. */ getTxExplorerUrl(txid: string): string { - return `${this.explorerBaseUrl}/tx/${txid.trim()}` + return `${this.requireExplorerBaseUrl()}/tx/${txid.trim()}` } /** URL of the asset page on the block explorer (e.g. to open in a new tab). */ getAssetExplorerUrl(assetId: string): string { - return `${this.explorerBaseUrl}/asset/${assetId.trim()}` + return `${this.requireExplorerBaseUrl()}/asset/${assetId.trim()}` } /** Latest block hash (tip). */ @@ -340,8 +355,13 @@ export interface ScripthashTxEntry { export interface EsploraVout { scriptpubkey?: string scriptpubkey_hex?: string + scriptpubkey_address?: string value?: number + valuecommitment?: string asset?: string + assetcommitment?: string + nonce?: string + noncecommitment?: string [key: string]: unknown } diff --git a/web/src/components/AccountMenu.tsx b/web/src/components/AccountMenu.tsx deleted file mode 100644 index 410d600..0000000 --- a/web/src/components/AccountMenu.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { useState, useRef, useEffect } from 'react' -import { CopyIcon } from './CopyIcon' -import { ButtonPrimary, ButtonIconNeutral } from './Button' - -const P2PK_NETWORK: 'testnet' | 'mainnet' = 'testnet' -const EXPLORER_ADDRESS_URL = 'https://blockstream.info/liquidtestnet/address/' - -function shortAddress(addr: string, head = 8, tail = 3): string { - if (addr.length <= head + tail) return addr - return addr.slice(0, head) + '…' + addr.slice(-tail) -} - -function ExternalLinkIcon({ className }: { className?: string }) { - return ( - - - - ) -} - -type Props = { - accountIndex: number - accountAddress: string | null - addressLoading: boolean - onAccountIndexChange: (index: number) => void - onDisconnect: () => void -} - -export function AccountMenu({ - accountIndex, - accountAddress, - addressLoading, - onAccountIndexChange, - onDisconnect, -}: Props) { - const [isOpen, setIsOpen] = useState(false) - const [indexInput, setIndexInput] = useState(String(accountIndex)) - const menuRef = useRef(null) - - useEffect(() => { - setIndexInput(String(accountIndex)) - }, [accountIndex]) - - useEffect(() => { - if (!isOpen) return - const handleClickOutside = (e: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(e.target as Node)) { - setIsOpen(false) - } - } - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, [isOpen]) - - const handleCopyAddress = () => { - if (accountAddress) { - void navigator.clipboard.writeText(accountAddress) - } - } - - const handleApplyIndex = () => { - const n = parseInt(indexInput, 10) - if (!Number.isNaN(n) && n >= 0) { - onAccountIndexChange(n) - } else { - setIndexInput(String(accountIndex)) - } - } - - const handleDisconnect = () => { - setIsOpen(false) - onDisconnect() - } - - const label = addressLoading - ? 'Loading…' - : accountAddress - ? shortAddress(accountAddress) - : `Account ${accountIndex}` - - return ( -
- - - {isOpen && ( -
-
-
-

- Address (P2PK, {P2PK_NETWORK}) -

- {addressLoading ? ( -

Loading…

- ) : accountAddress ? ( -
- - {accountAddress} - - - - - - - -
- ) : ( -

Failed to load address

- )} -
- -
-

Account index

-
- setIndexInput(e.target.value)} - onBlur={handleApplyIndex} - onKeyDown={(e) => e.key === 'Enter' && handleApplyIndex()} - className="w-24 rounded border border-gray-300 px-2 py-1.5 text-sm text-gray-900 - [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]" - /> - - Switch - -
-
- -
- - Disconnect - -
-
-
- )} -
- ) -} diff --git a/web/src/components/OfferTable.tsx b/web/src/components/OfferTable.tsx index d84818a..a90c6cb 100644 --- a/web/src/components/OfferTable.tsx +++ b/web/src/components/OfferTable.tsx @@ -7,6 +7,7 @@ import { useMemo } from 'react' import { EsploraClient } from '../api/esplora' import type { OfferShort } from '../types/offers' +import { CopyIcon } from './CopyIcon' import { OfferStatusBadge } from './OfferStatusBadge' const BLOCKS_PER_DAY_LIQUID = 1440 @@ -17,6 +18,10 @@ function shortId(id: string, len = 12): string { return `${id.slice(0, 8)}…${id.slice(-4)}` } +function isInteractiveTarget(target: HTMLElement | null): boolean { + return target?.closest('a, button, input, textarea, select') != null +} + function formatSats(amount: bigint): string { const s = String(amount) if (amount <= BigInt(MAX_SAFE_INTEGER)) return Number(amount).toLocaleString() @@ -142,25 +147,41 @@ export function OfferTable({ onClick={ onOfferClick != null ? (e) => { - const target = e.target as HTMLElement - if (target.closest('a')) return - onOfferClick(offer) - } + const target = e.target as HTMLElement + if (isInteractiveTarget(target)) return + onOfferClick(offer) + } : undefined } onKeyDown={ onOfferClick != null ? (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - onOfferClick(offer) - } + const target = e.target as HTMLElement + if (isInteractiveTarget(target)) return + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onOfferClick(offer) } + } : undefined } > - {shortId(offer.id)} +
+ {shortId(offer.id)} + +
void - /** How option value is stored: index in utxos array, or "txid:vout". */ - optionValueType: UtxoSelectValueType - /** Placeholder / empty option label (e.g. "Select UTXO…"). */ - placeholder?: string - /** Label suffix after value, e.g. "sats" or "(asset)". */ - labelSuffix?: string - className?: string - id?: string - disabled?: boolean - /** When true, width follows content (for long UTXO labels). */ - adaptiveWidth?: boolean -} - -export function UtxoSelect({ - utxos, - value, - onChange, - optionValueType, - placeholder, - labelSuffix = 'sats', - className, - id, - disabled, - adaptiveWidth, -}: UtxoSelectProps) { - const sorted = - optionValueType === 'index' - ? utxos - .map((u, idx) => ({ u, idx })) - .sort((a, b) => { - const c = a.u.txid.localeCompare(b.u.txid) - return c !== 0 ? c : a.u.vout - b.u.vout - }) - : [...utxos].sort((a, b) => { - const c = a.txid.localeCompare(b.txid) - return c !== 0 ? c : a.vout - b.vout - }) - - const options = - optionValueType === 'index' - ? (sorted as { u: ScripthashUtxoEntry; idx: number }[]).map(({ u, idx }) => ({ - value: String(idx), - label: formatUtxoOptionLabel(u, { suffix: labelSuffix }), - })) - : (sorted as ScripthashUtxoEntry[]).map((u) => ({ - value: `${u.txid}:${u.vout}`, - label: formatUtxoOptionLabel(u, { suffix: labelSuffix }), - })) - - const selectOptions = placeholder ? [{ value: '', label: placeholder }, ...options] : options - - return ( - setPrepareDestinationAddress(event.target.value)} + placeholder="Receive address" + /> + +

+ This requests one wallet-funded transaction that creates four 100-sat LBTC outputs for + the issuance step. Use a receive address from the connected wallet. +

+ + + + +
+
+ + setIssueCollateralAmount(event.target.value)} + suffix="sats" + /> + + + setIssuePrincipalAmount(event.target.value)} + suffix="sats" + /> + + + setIssueLoanExpiration(event.target.value)} + /> + + + setIssueInterestPercent(event.target.value)} + suffix="%" + /> + +
+ + setIssueDestinationAddress(event.target.value)} + placeholder="Receive address" + /> + + + +
+ +
+ {issuancePreview ? ( +
+

Decoded issuance parameters

+

+ Collateral {formatSats(issuancePreview.terms.collateralAmount)} sats, principal{' '} + {formatSats(issuancePreview.terms.principalAmount)} sats, interest{' '} + {(issuancePreview.terms.principalInterestRate / 100).toFixed(2)}%, expiry block{' '} + {issuancePreview.terms.loanExpirationTime.toLocaleString()}. +

+
+ ) : issuancePreviewError ? ( +

+ {issuancePreviewError} +

+ ) : ( +

+ Complete the issuance step to load the decoded parameters here. +

+ )} + + setCreateCollateralAssetId(event.target.value)} + className="font-mono" + /> + + + setCreatePrincipalAssetId(event.target.value)} + className="font-mono" + /> + + + setBorrowerDestinationAddress(event.target.value)} + /> + + + +
+ + +
+
+ + setTrackOfferId(event.target.value)} + placeholder="Offer ID" + /> + + +
+ {trackOfferError ? ( +

+ {trackOfferError} +

+ ) : null} + {trackOfferSuccess ? ( +

+ {trackOfferSuccess} +

+ ) : null} + void loadOffers()} + emptyMessage="No borrower offers found for this wallet" + onOfferClick={(offer) => { + setSelectedOffer(offer) + setCancelState(emptyActionState()) + setRepayState(emptyActionState()) + }} + /> +
+ + {selectedOffer && ( +
+
+
+

Status

+

{selectedOffer.status}

+
+
+

Terms

+

+ Collateral {formatSats(selectedOffer.collateral_amount)} sats, principal{' '} + {formatSats(selectedOffer.principal_amount)} sats, interest{' '} + {(selectedOffer.interest_rate / 100).toFixed(2)}%. +

+
+
+ + {selectedOffer.status === 'pending' && ( +
+ + setCancelDestinationAddress(event.target.value)} + /> + + + +
+ )} + + {selectedOffer.status === 'active' && ( +
+ + setRepayDestinationAddress(event.target.value)} + /> + + + +
+ )} + + {selectedOffer.status !== 'pending' && selectedOffer.status !== 'active' && ( +

+ This offer does not have a borrower-side action in the web client. +

+ )} +
+ )} + + ) +} diff --git a/web/src/pages/CreateOffer/CancelOfferModal.tsx b/web/src/pages/CreateOffer/CancelOfferModal.tsx deleted file mode 100644 index 2ff37f9..0000000 --- a/web/src/pages/CreateOffer/CancelOfferModal.tsx +++ /dev/null @@ -1,350 +0,0 @@ -/** - * Modal to cancel a pending offer (borrower/creator only): spend PreLock + 4 NFTs + fee, - * return collateral to the chosen address. Only for offers with status pending. - */ - -import { useMemo, useState, useEffect, useRef } from 'react' -import { Modal } from '../../components/Modal' -import { TxActionButtons } from '../../components/TxActionButtons' -import { TxStatusBlock } from '../../components/TxStatusBlock' -import { BroadcastStatusContent } from '../../components/PostBroadcastModal' -import { getBroadcastSuccessMessage } from '../../components/broadcastSuccessMessages' -import { InfoTooltip } from '../../components/InfoTooltip' -import { Input } from '../../components/Input' -import { UtxoSelect } from '../../components/UtxoSelect' -import { formClassNames } from '../../components/formClassNames' -import type { OfferShort } from '../../types/offers' -import type { ScripthashUtxoEntry } from '../../api/esplora' -import type { EsploraClient } from '../../api/esplora' -import { EsploraApiError } from '../../api/esplora' -import { formatBroadcastError } from '../../utils/parseBroadcastError' -import { - P2PK_NETWORK, - POLICY_ASSET_ID, - getScriptPubkeyHexFromAddress, -} from '../../utility/addressP2pk' -import { - buildPreLockCancellationTx, - type BuildPreLockCancellationTxResult, - type CancellationUtxo, -} from '../../tx/preLockCancellation/buildPreLockCancellationTx' -import { buildPreLockArgumentsFromOfferCreation } from '../../utility/preLockCovenants' -import { finalizePreLockCancellationTx } from '../../tx/preLockCancellation/finalizePreLockCancellationTx' -import { parseSeedHex, deriveSecretKeyFromIndex } from '../../utility/seed' -import type { PsetWithExtractTx } from '../../simplicity' -import type { PreLockArguments } from '../../utility/preLockArguments' - -export interface CancelOfferModalProps { - offer: OfferShort - utxos: ScripthashUtxoEntry[] - esplora: EsploraClient - open: boolean - onClose: () => void - accountAddress: string | null - seedHex?: string | null - accountIndex?: number - onSuccess?: () => void -} - -export function CancelOfferModal({ - offer, - utxos, - esplora, - open, - onClose, - accountAddress, - seedHex: seedHexProp = null, - accountIndex = 0, - onSuccess, -}: CancelOfferModalProps) { - const [creationTx, setCreationTx] = useState> | null>( - null - ) - const [creationTxError, setCreationTxError] = useState(null) - const [destinationAddress, setDestinationAddress] = useState('') - const [feeUtxoIndex, setFeeUtxoIndex] = useState(0) - const [feeAmount, setFeeAmount] = useState('500') - const [building, setBuilding] = useState(false) - const [buildError, setBuildError] = useState(null) - const [preLockArguments, setPreLockArguments] = useState(null) - const [builtTx, setBuiltTx] = useState(null) - const [signedTxHex, setSignedTxHex] = useState(null) - const [broadcastTxid, setBroadcastTxid] = useState(null) - - const nativeUtxos = useMemo(() => { - const policyId = POLICY_ASSET_ID[P2PK_NETWORK] - return utxos.filter((u) => !u.asset || u.asset.trim().toLowerCase() === policyId) - }, [utxos]) - - useEffect(() => { - if (open && offer.id && offer.status === 'pending' && offer.created_at_txid) { - setCreationTx(null) - setCreationTxError(null) - setBuildError(null) - setBuiltTx(null) - setSignedTxHex(null) - setBroadcastTxid(null) - setDestinationAddress(accountAddress ?? '') - setPreLockArguments(null) - esplora - .getTx(offer.created_at_txid) - .then(setCreationTx) - .catch((e) => { - setCreationTxError(e instanceof Error ? e.message : String(e)) - }) - } - }, [open, offer.id, offer.status, offer.created_at_txid, accountAddress, esplora]) - - const handleBuild = async () => { - if (offer.status !== 'pending') { - setBuildError('Only pending offers can be cancelled.') - return - } - if (!creationTx) { - setBuildError('Offer creation tx not loaded.') - return - } - - const feeEntry = nativeUtxos[feeUtxoIndex] - if (!feeEntry) { - setBuildError('Select a fee UTXO (LBTC).') - return - } - - const feeNum = parseInt(feeAmount, 10) || 0 - if (feeNum <= 0) { - setBuildError('Fee amount must be at least 1.') - return - } - - const dest = destinationAddress.trim() - if (!dest) { - setBuildError('Enter collateral destination address.') - return - } - - setBuildError(null) - setBuiltTx(null) - setSignedTxHex(null) - setBroadcastTxid(null) - setBuilding(true) - try { - const collateralOutputScriptHex = await getScriptPubkeyHexFromAddress(dest) - - const feeTx = await esplora.getTx(feeEntry.txid) - const feePrevout = feeTx.vout?.[feeEntry.vout] - if (!feePrevout || feePrevout.value == null) { - throw new Error('Fee UTXO prevout not found or confidential.') - } - - const feeUtxo: CancellationUtxo = { - outpoint: { txid: feeEntry.txid, vout: feeEntry.vout }, - prevout: feePrevout, - } - - const { preLockArguments } = await buildPreLockArgumentsFromOfferCreation( - offer, - creationTx, - P2PK_NETWORK - ) - - const result = await buildPreLockCancellationTx({ - offer, - offerCreationTx: creationTx, - collateralOutputScriptHex, - feeUtxo, - feeAmount: BigInt(feeNum), - preLockArguments, - network: P2PK_NETWORK, - }) - - setPreLockArguments(preLockArguments) - setBuiltTx(result) - } catch (e) { - setBuildError(e instanceof Error ? e.message : String(e)) - } finally { - setBuilding(false) - } - } - - const handleSign = async () => { - if (!builtTx) { - setBuildError('Build transaction first.') - return - } - if (!preLockArguments) { - setBuildError('Build transaction first.') - return - } - if (!seedHexProp) { - setBuildError('Seed is required to sign.') - return - } - - setBuildError(null) - setBuilding(true) - try { - const secretKey = deriveSecretKeyFromIndex(parseSeedHex(seedHexProp), accountIndex) - const signed = await finalizePreLockCancellationTx({ - pset: builtTx.pset as PsetWithExtractTx, - prevouts: builtTx.prevouts, - preLockArguments, - network: 'testnet', - borrowerSecretKey: secretKey, - }) - setSignedTxHex(signed.signedTxHex) - setBroadcastTxid(null) - } catch (e) { - setBuildError(e instanceof Error ? e.message : String(e)) - } finally { - setBuilding(false) - } - } - - const handleBroadcast = async () => { - if (!signedTxHex) { - setBuildError('Sign transaction first.') - return - } - setBuildError(null) - setBuilding(true) - try { - const txid = await esplora.broadcastTx(signedTxHex) - setBroadcastTxid(txid) - } catch (e) { - if (e instanceof EsploraApiError) { - setBuildError(formatBroadcastError(e.body ?? e.message)) - } else { - setBuildError(e instanceof Error ? e.message : String(e)) - } - } finally { - setBuilding(false) - } - } - - const handleClose = () => { - if (broadcastTxid) { - onSuccess?.() - } - setBroadcastTxid(null) - onClose() - } - - const bottomAnchorRef = useRef(null) - useEffect(() => { - if (buildError || builtTx || signedTxHex) { - bottomAnchorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) - } - }, [buildError, builtTx, signedTxHex]) - - if (open && offer.status !== 'pending') { - return ( - -

- Only pending offers can be cancelled. This offer has status "{offer.status}". -

-
- ) - } - - return ( - - {broadcastTxid ? ( - - ) : ( -
-

- Cancel this pending offer and get your locked collateral back. Collateral will be sent - to the destination address below (default: your address). -

- - {!creationTx && !creationTxError && ( -

Loading offer creation tx…

- )} - {creationTxError && ( -

{creationTxError}

- )} - - {creationTx && ( - <> -
-
-

- Collateral destination -

- -
- setDestinationAddress(e.target.value)} - /> -
- -
-
-

- Transaction details -

- -
-
-
-

Fee UTXO (LBTC)

- {nativeUtxos.length === 0 ? ( -

No LBTC UTXOs.

- ) : ( - setFeeUtxoIndex(parseInt(v, 10))} - optionValueType="index" - labelSuffix="sats" - /> - )} -
-
-

Fee amount (sats)

- setFeeAmount(e.target.value)} - /> -
-
-
- - void handleBuild()} - onSign={() => void handleSign()} - onSignAndBroadcast={() => void handleBroadcast()} - broadcastButtonLabel="Cancel offer" - canBuild={nativeUtxos.length > 0} - /> - - - - )} - - ) -} diff --git a/web/src/pages/CreateOffer/CreateOfferWizard.tsx b/web/src/pages/CreateOffer/CreateOfferWizard.tsx deleted file mode 100644 index e9fd5d0..0000000 --- a/web/src/pages/CreateOffer/CreateOfferWizard.tsx +++ /dev/null @@ -1,191 +0,0 @@ -/** - * 2-step wizard for Create Offer: Issue Utility NFTs → Create PreLock. - * Prepare is done outside the wizard (Borrower page "Prepare to be a borrower"). - * Steps shown as pills with arrow; step 2 disabled until step 1 done; summary on step 1 when returning. - */ - -import { useState } from 'react' -import type { ReactNode } from 'react' -import type { EsploraClient } from '../../api/esplora' -import type { ScripthashUtxoEntry } from '../../api/esplora' -import { PostBroadcastModal } from '../../components/PostBroadcastModal' -import { getBroadcastSuccessMessage } from '../../components/broadcastSuccessMessages' -import { IssueUtilityNftsStep } from './IssueUtilityNftsStep' -import { FinalizeOfferStep } from './FinalizeOfferStep' - -export type CreateOfferStep = 2 | 3 - -/** Summary data from step 1 (Issue Utility NFTs) for display when user returns to step 1. */ -export interface Step1Summary { - txid: string - collateralAmount: string - principalAmount: string - feeAmount: string - loanExpirationTime: string - interestPercent: string - toAddress: string -} - -export interface CreateOfferWizardProps { - accountIndex: number - accountAddress: string | null - utxos: ScripthashUtxoEntry[] - esplora: EsploraClient - seedHex: string - savedPreparedTxid?: string | null - savedAuxiliaryAssetId?: string | null - savedPrepareFirstVout?: number - currentBlockHeight?: number | null - /** When set, wizard starts on step 2 (Create offer) with this issuance txid. */ - initialIssuanceTxid?: string | null - onBroadcastSuccess: () => void | Promise - onComplete?: () => void - /** Called when Issue Utility NFTs step succeeds; receives the issuance txid. */ - onIssueUtilityNftsSuccess?: (issuanceTxid: string) => void - /** When user clicks "Start over" on step 1 summary (clear issuance and close wizard). */ - onStartOver?: () => void - /** Optional control (e.g. Recover icon) shown in the same row as step pills, right-aligned. */ - recoveryControl?: ReactNode - /** Optional panel (e.g. recover-by-txid form) shown below the step pills row. */ - recoveryPanel?: ReactNode -} - -export function CreateOfferWizard({ - accountIndex, - accountAddress, - utxos, - esplora, - seedHex, - savedPreparedTxid = null, - savedAuxiliaryAssetId = null, - savedPrepareFirstVout = 0, - currentBlockHeight = null, - initialIssuanceTxid = null, - onBroadcastSuccess, - onComplete, - onIssueUtilityNftsSuccess, - onStartOver, - recoveryControl, - recoveryPanel, -}: CreateOfferWizardProps) { - const [currentStep, setCurrentStep] = useState(() => - initialIssuanceTxid?.trim() ? 3 : 2 - ) - /** Txid from completing step 1 in this session (null until then). Recovery uses initialIssuanceTxid. */ - const [step1CompletedTxid, setStep1CompletedTxid] = useState(null) - const [step1Summary, setStep1Summary] = useState(null) - /** Post-broadcast modal for step 1 (Issue Utility NFTs). When closed, advance to step 3. */ - const [postBroadcastStep1Txid, setPostBroadcastStep1Txid] = useState(null) - /** Post-broadcast modal for step 3 (Finalize offer). When closed, call onSuccess/onComplete. */ - const [postBroadcastStep3Txid, setPostBroadcastStep3Txid] = useState(null) - - const effectiveIssuanceTxid = initialIssuanceTxid ?? step1CompletedTxid - const step1Done = Boolean(effectiveIssuanceTxid?.trim()) - - const handleStep1Success = (txid: string, summary: Step1Summary) => { - setStep1CompletedTxid(txid) - setStep1Summary(summary) - void onBroadcastSuccess() - onIssueUtilityNftsSuccess?.(txid) - setPostBroadcastStep1Txid(txid) - } - - const handlePostBroadcastStep1Close = () => { - setPostBroadcastStep1Txid(null) - setCurrentStep(3) - } - - const handlePostBroadcastStep3Close = () => { - setPostBroadcastStep3Txid(null) - void onBroadcastSuccess() - onComplete?.() - } - - return ( -
-
-
- - - → - - -
- {recoveryControl != null ?
{recoveryControl}
: null} -
- {recoveryPanel != null ?
{recoveryPanel}
: null} - - - - - - {currentStep === 2 && ( - - )} - - {currentStep === 3 && ( - - )} -
- ) -} diff --git a/web/src/pages/CreateOffer/FinalizeOfferStep.tsx b/web/src/pages/CreateOffer/FinalizeOfferStep.tsx deleted file mode 100644 index adf1bb7..0000000 --- a/web/src/pages/CreateOffer/FinalizeOfferStep.tsx +++ /dev/null @@ -1,530 +0,0 @@ -/** - * Step 2: Finalize offer (PreLock creation). Form + data loading from issuance tx, validation, build flow. - * NFT outpoints from Step 1 (Issue Utility NFTs): same txid, vout 0=Borrower, 1=Lender, 2=First params, 3=Second params. - */ - -import { useState, useMemo, useEffect, useCallback, useRef } from 'react' -import type { EsploraClient } from '../../api/esplora' -import type { ScripthashUtxoEntry } from '../../api/esplora' -import type { EsploraTx } from '../../api/esplora' -import { EsploraApiError } from '../../api/esplora' -import { formatBroadcastError } from '../../utils/parseBroadcastError' -import { P2PK_NETWORK, POLICY_ASSET_ID, getP2pkAddressFromSecret } from '../../utility/addressP2pk' -import { parseSeedHex, deriveSecretKeyFromIndex } from '../../utility/seed' -import { buildLendingParamsFromParameterNFTs } from '../../utility/parametersEncoding' -import { buildPreLockArguments } from '../../utility/preLockArguments' -import { hexToBytes32, normalizeHex, assetIdDisplayToInternal } from '../../utility/hex' -import { computePreLockCovenantHashes } from '../../utility/preLockCovenants' -import type { PsetWithExtractTx } from '../../simplicity' -import { - buildPreLockCreationTx, - finalizePreLockCreationTx, -} from '../../tx/preLockCreation/buildPreLockCreationTx' -import { TxActionButtons } from '../../components/TxActionButtons' -import { TxStatusBlock } from '../../components/TxStatusBlock' -import { Input } from '../../components/Input' -import { UtxoSelect } from '../../components/UtxoSelect' -import { formClassNames } from '../../components/formClassNames' - -export interface FinalizeOfferStepProps { - accountIndex: number - accountAddress: string | null - utxos: ScripthashUtxoEntry[] - esplora: EsploraClient - seedHex: string - issuanceTxid: string | null - onSuccess: () => void - /** When provided, called with txid after broadcast instead of onSuccess; parent shows post-broadcast modal and calls onSuccess when modal closes. */ - onBroadcastTxid?: (txid: string) => void -} - -/** Order in Issue Utility NFTs tx: vout 0=Borrower, 1=Lender, 2=First params, 3=Second params. */ -const NFT_VOUT_ORDER = [ - { label: 'First parameters NFT', vout: 2 }, - { label: 'Second parameters NFT', vout: 3 }, - { label: 'Borrower NFT', vout: 0 }, - { label: 'Lender NFT', vout: 1 }, -] as const - -export function FinalizeOfferStep({ - accountAddress, - utxos, - issuanceTxid, - esplora, - seedHex, - accountIndex, - onSuccess, - onBroadcastTxid, -}: FinalizeOfferStepProps) { - const [collateralUtxoIndex, setCollateralUtxoIndex] = useState(0) - const [principalAssetIdHex, setPrincipalAssetIdHex] = useState('') - const [issuanceTxId, setIssuanceTxId] = useState(() => issuanceTxid ?? '') - const [feeUtxoIndex, setFeeUtxoIndex] = useState(0) - const [feeAmount, setFeeAmount] = useState('') - const [toAddress, setToAddress] = useState(accountAddress ?? '') - const [stubMessage, setStubMessage] = useState(null) - const [buildError, setBuildError] = useState(null) - const [issuanceTx, setIssuanceTx] = useState(null) - const [lendingParams, setLendingParams] = useState | null>(null) - const [loadingIssuance, setLoadingIssuance] = useState(false) - const [issuanceLoadError, setIssuanceLoadError] = useState(null) - const [building, setBuilding] = useState(false) - const [builtPreLockTx, setBuiltPreLockTx] = useState - > | null>(null) - const [signedTxHex, setSignedTxHex] = useState(null) - const [broadcastError, setBroadcastError] = useState(null) - - const bottomAnchorRef = useRef(null) - useEffect(() => { - if (buildError || broadcastError || signedTxHex) { - bottomAnchorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) - } - }, [buildError, broadcastError, signedTxHex]) - - useEffect(() => { - const txid = issuanceTxId.trim() - if (!txid) { - setIssuanceTx(null) - setLendingParams(null) - setIssuanceLoadError(null) - return - } - let cancelled = false - setLoadingIssuance(true) - setIssuanceLoadError(null) - esplora - .getTx(txid) - .then((tx) => { - if (cancelled) return - const vout = tx.vout ?? [] - if (vout.length < 4) { - setIssuanceLoadError('Issuance tx must have at least 4 vouts') - setIssuanceTx(null) - setLendingParams(null) - return - } - const v2 = vout[2]?.value - const v3 = vout[3]?.value - if (typeof v2 !== 'number' || typeof v3 !== 'number') { - setIssuanceLoadError('Vout 2 and 3 must have explicit value') - setIssuanceTx(null) - setLendingParams(null) - return - } - try { - const params = buildLendingParamsFromParameterNFTs(BigInt(v2), BigInt(v3)) - setLendingParams(params) - setIssuanceTx(tx) - } catch (e) { - setIssuanceLoadError(e instanceof Error ? e.message : String(e)) - setLendingParams(null) - setIssuanceTx(null) - } - }) - .catch((e) => { - if (!cancelled) { - setIssuanceLoadError(e instanceof Error ? e.message : String(e)) - setIssuanceTx(null) - setLendingParams(null) - } - }) - .finally(() => { - if (!cancelled) setLoadingIssuance(false) - }) - return () => { - cancelled = true - } - }, [esplora, issuanceTxId]) - - const nativeUtxos = useMemo(() => { - const policyId = POLICY_ASSET_ID[P2PK_NETWORK] - return utxos.filter((u) => !u.asset || u.asset.trim().toLowerCase() === policyId) - }, [utxos]) - - const handleBuild = useCallback(async () => { - setBuildError(null) - setStubMessage(null) - setBuiltPreLockTx(null) - setSignedTxHex(null) - const principalHex = normalizeHex(principalAssetIdHex) - if (principalHex.length !== 64) { - setBuildError('Principal asset ID must be 64 hex chars (32 bytes).') - return - } - if (!toAddress.trim()) { - setBuildError('To address is required.') - return - } - if (!lendingParams || !issuanceTx?.vout?.length) { - setBuildError('Load issuance tx first (enter txid and wait for loading).') - return - } - const collateralUtxo = nativeUtxos[collateralUtxoIndex] - if (!collateralUtxo) { - setBuildError('Select a collateral UTXO (LBTC).') - return - } - if (collateralUtxoIndex === feeUtxoIndex) { - setBuildError('Collateral and Fee must be different UTXOs.') - return - } - const collateralValue = BigInt(collateralUtxo.value ?? 0) - if (collateralValue < lendingParams.collateralAmount) { - setBuildError( - `Collateral UTXO value ${collateralValue} is less than required ${lendingParams.collateralAmount}.` - ) - return - } - const feeNum = parseInt(feeAmount, 10) || 0 - if (feeNum <= 0) { - setBuildError('Fee amount must be at least 1.') - return - } - const feeUtxo = nativeUtxos[feeUtxoIndex] - if (!feeUtxo || BigInt(feeUtxo.value ?? 0) < BigInt(feeNum)) { - setBuildError('Fee UTXO has insufficient value.') - return - } - if (!seedHex) { - setBuildError('Seed is required for borrower pubkey derivation.') - return - } - - setBuilding(true) - try { - const secretKey = deriveSecretKeyFromIndex(parseSeedHex(seedHex), accountIndex) - const { internalKeyHex } = await getP2pkAddressFromSecret(secretKey, P2PK_NETWORK) - const borrowerPubKey = hexToBytes32(internalKeyHex) - - const vout = issuanceTx.vout! - const policyAssetHex = POLICY_ASSET_ID[P2PK_NETWORK] - const collateralAssetId = assetIdDisplayToInternal(policyAssetHex) - const hexTo32 = (h: string) => { - const n = normalizeHex(h) - if (n.length !== 64) throw new Error('Expected 64 hex chars') - return assetIdDisplayToInternal(n) - } - const principalAssetId = assetIdDisplayToInternal(principalHex) - const borrowerNftAssetId = hexTo32(String(vout[0]?.asset ?? '')) - const lenderNftAssetId = hexTo32(String(vout[1]?.asset ?? '')) - const firstParamsNftAssetId = hexTo32(String(vout[2]?.asset ?? '')) - const secondParamsNftAssetId = hexTo32(String(vout[3]?.asset ?? '')) - - const covenantResult = await computePreLockCovenantHashes({ - collateralAssetId, - principalAssetId, - borrowerNftAssetId, - lenderNftAssetId, - firstParametersNftAssetId: firstParamsNftAssetId, - secondParametersNftAssetId: secondParamsNftAssetId, - lendingParams, - borrowerPubKey, - network: P2PK_NETWORK, - }) - - const preLockArguments = buildPreLockArguments({ - collateralAssetId, - principalAssetId, - borrowerNftAssetId, - lenderNftAssetId, - firstParametersNftAssetId: firstParamsNftAssetId, - secondParametersNftAssetId: secondParamsNftAssetId, - lendingCovHash: covenantResult.lendingCovHash, - parametersNftOutputScriptHash: covenantResult.parametersNftOutputScriptHash, - borrowerNftOutputScriptHash: covenantResult.borrowerP2trScriptHash, - principalOutputScriptHash: covenantResult.borrowerP2trScriptHash, - borrowerPubKey, - lendingParams, - }) - - const issuanceTxidTrimmed = issuanceTxId.trim() - const v0 = vout[0] - const v1 = vout[1] - const v2 = vout[2] - const v3 = vout[3] - if (!v0 || !v1 || !v2 || !v3) { - setBuildError('Issuance tx must have vouts 0–3.') - return - } - - const collateralTx = await esplora.getTx(collateralUtxo.txid) - const collateralPrevout = collateralTx.vout?.[collateralUtxo.vout] - if (!collateralPrevout) { - setBuildError('Collateral UTXO prevout not found.') - return - } - const feeTx = await esplora.getTx(feeUtxo.txid) - const feePrevout = feeTx.vout?.[feeUtxo.vout] - if (!feePrevout) { - setBuildError('Fee UTXO prevout not found.') - return - } - - const result = await buildPreLockCreationTx({ - collateralUtxo: { - outpoint: { txid: collateralUtxo.txid, vout: collateralUtxo.vout }, - prevout: collateralPrevout, - }, - firstParametersNftUtxo: { - outpoint: { txid: issuanceTxidTrimmed, vout: 2 }, - prevout: v2, - }, - secondParametersNftUtxo: { - outpoint: { txid: issuanceTxidTrimmed, vout: 3 }, - prevout: v3, - }, - borrowerNftUtxo: { - outpoint: { txid: issuanceTxidTrimmed, vout: 0 }, - prevout: v0, - }, - lenderNftUtxo: { - outpoint: { txid: issuanceTxidTrimmed, vout: 1 }, - prevout: v1, - }, - feeUtxo: { - outpoint: { txid: feeUtxo.txid, vout: feeUtxo.vout }, - prevout: feePrevout, - }, - preLockArguments, - preLockScriptPubkeyHex: covenantResult.preLockScriptPubkeyHex, - utilityNftsOutputScriptHex: covenantResult.utilityNftsOutputScriptHex, - feeAmount: BigInt(feeNum), - network: P2PK_NETWORK, - }) - - setBuiltPreLockTx(result) - } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - if ( - msg.includes('Backend or WASM') || - msg.includes('require Simplicity') || - msg.includes('async LWK') - ) { - setStubMessage( - 'PreLock creation requires LWK Simplicity (WASM). Use the CLI or ensure lwk_web is loaded.' - ) - } else { - setBuildError(msg) - } - } finally { - setBuilding(false) - } - }, [ - principalAssetIdHex, - toAddress, - lendingParams, - issuanceTx, - issuanceTxId, - nativeUtxos, - collateralUtxoIndex, - feeUtxoIndex, - feeAmount, - seedHex, - accountIndex, - esplora, - ]) - - const handleSignAndBroadcast = useCallback( - async (broadcast: boolean) => { - if (!builtPreLockTx) { - setBuildError('Build the transaction first (click Build).') - return - } - if (!seedHex) { - setBuildError('Seed is required to sign.') - return - } - setBuildError(null) - setBroadcastError(null) - setBuilding(true) - try { - const secretKey = deriveSecretKeyFromIndex(parseSeedHex(seedHex), accountIndex) - const { signedTxHex: hex } = await finalizePreLockCreationTx({ - pset: builtPreLockTx.pset as PsetWithExtractTx, - prevouts: builtPreLockTx.prevouts, - secretKey, - network: P2PK_NETWORK, - }) - setSignedTxHex(hex) - if (broadcast) { - const txid = await esplora.broadcastTx(hex) - if (onBroadcastTxid) { - onBroadcastTxid(txid) - } else { - onSuccess() - } - } - } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - if (e instanceof EsploraApiError) { - setBroadcastError(formatBroadcastError(e.body ?? e.message)) - } else { - setBuildError(msg) - } - } finally { - setBuilding(false) - } - }, - [builtPreLockTx, seedHex, accountIndex, esplora, onSuccess, onBroadcastTxid] - ) - - const handleBroadcast = useCallback(async () => { - if (!signedTxHex) { - setBuildError('Sign the transaction first.') - return - } - setBuildError(null) - setBroadcastError(null) - setBuilding(true) - try { - const txid = await esplora.broadcastTx(signedTxHex) - if (onBroadcastTxid) { - onBroadcastTxid(txid) - } else { - onSuccess() - } - } catch (e) { - if (e instanceof EsploraApiError) { - setBroadcastError(formatBroadcastError(e.body ?? e.message)) - } else { - setBuildError(e instanceof Error ? e.message : String(e)) - } - } finally { - setBuilding(false) - } - }, [signedTxHex, esplora, onSuccess, onBroadcastTxid]) - - return ( -
-

Step 2: Finalize offer

-
-
-

Collateral UTXO (LBTC)

- {nativeUtxos.length === 0 ? ( -

No LBTC UTXOs.

- ) : ( - setCollateralUtxoIndex(parseInt(v, 10))} - optionValueType="index" - labelSuffix="sats" - /> - )} -
- -
-

Principal asset ID (hex, big-endian)

- setPrincipalAssetIdHex(e.target.value)} - /> -
- -
-

Issuance tx id (from Step 1)

- setIssuanceTxId(e.target.value)} - /> - {loadingIssuance &&

Loading issuance tx…

} - {issuanceLoadError &&

{issuanceLoadError}

} - {lendingParams && !issuanceLoadError && ( -

- Collateral: {String(lendingParams.collateralAmount)} · Principal:{' '} - {String(lendingParams.principalAmount)} · Expiry block:{' '} - {lendingParams.loanExpirationTime} · Interest: {lendingParams.principalInterestRate}{' '} - bp -

- )} -

- NFT outpoints: (txid, 0)=Borrower, (txid, 1)=Lender, (txid, 2)=First params, (txid, - 3)=Second params. -

-
- - {NFT_VOUT_ORDER.map(({ label, vout }) => ( -
-

- {label} (vout {vout}) -

-

- {issuanceTxId ? `${issuanceTxId.slice(0, 20)}…:${vout}` : '—'} -

-
- ))} - -
-

Fee UTXO (LBTC)

- {nativeUtxos.length === 0 ? ( -

No LBTC UTXOs.

- ) : ( - setFeeUtxoIndex(parseInt(v, 10))} - optionValueType="index" - labelSuffix="sats" - /> - )} -
- -
-

Fee amount (sats)

- setFeeAmount(e.target.value)} - /> -
- -
-

To address

- setToAddress(e.target.value)} - /> -
- -
- void handleBuild()} - onSign={() => void handleSignAndBroadcast(false)} - onSignAndBroadcast={handleBroadcast} - broadcastButtonLabel="Sign & Broadcast" - /> -
- - - {stubMessage && ( -

- {stubMessage} -

- )} - -
- ) -} diff --git a/web/src/pages/CreateOffer/IssueUtilityNftsStep.tsx b/web/src/pages/CreateOffer/IssueUtilityNftsStep.tsx deleted file mode 100644 index 4da801c..0000000 --- a/web/src/pages/CreateOffer/IssueUtilityNftsStep.tsx +++ /dev/null @@ -1,787 +0,0 @@ -/** - * Step 2: Issue Utility NFTs. Builds and signs (or broadcasts) the tx per utility_nfts_issuing.rs. - * Uses 4 auxiliary UTXOs (from prepare at firstVout..firstVout+3) + 1 fee UTXO; generates fresh issuance entropy. - */ - -import { useState, useMemo, useCallback, useEffect, useRef } from 'react' -import type { EsploraClient } from '../../api/esplora' -import type { ScripthashUtxoEntry } from '../../api/esplora' -import { EsploraApiError } from '../../api/esplora' -import { formatBroadcastError } from '../../utils/parseBroadcastError' -import { P2PK_NETWORK, POLICY_ASSET_ID } from '../../utility/addressP2pk' -import { parseSeedHex, deriveSecretKeyFromIndex } from '../../utility/seed' -import { - percentToBasisPoints, - toBaseAmount, - encodeFirstNFTParameters, - encodeSecondNFTParameters, - decodeFirstNFTParameters, - decodeSecondNFTParameters, -} from '../../utility/parametersEncoding' -import type { PsetWithExtractTx } from '../../simplicity' -import { - buildIssueUtilityNftsTx, - finalizeIssueUtilityNftsTx, -} from '../../tx/issueUtilityNfts/buildIssueUtilityNftsTx' -import { TxActionButtons } from '../../components/TxActionButtons' -import { TxStatusBlock } from '../../components/TxStatusBlock' -import { ButtonSecondary } from '../../components/Button' -import { Input } from '../../components/Input' -import { UtxoSelect } from '../../components/UtxoSelect' -import { formClassNames } from '../../components/formClassNames' -import { InfoTooltip } from '../../components/InfoTooltip' -import type { Step1Summary } from './CreateOfferWizard' - -function CopyIcon({ className }: { className?: string }) { - return ( - - - - - ) -} - -const TOKEN_DECIMALS = 1 -/** Wider tooltip on step 1 (no min-width so modal size is unchanged). */ -const STEP1_TOOLTIP_CLASS = 'max-w-[22rem]' - -function prepareVouts(firstVout: number): [number, number, number, number] { - return [firstVout, firstVout + 1, firstVout + 2, firstVout + 3] -} -const BLOCK_TIME_MINUTES = 1 - -function formatEndsIn(blockHeight: number, currentHeight: number | null): string { - if (currentHeight == null) return '' - const delta = blockHeight - currentHeight - if (delta <= 0) return 'Already passed or current' - const totalMinutes = delta * BLOCK_TIME_MINUTES - const days = Math.floor(totalMinutes / (60 * 24)) - const hours = Math.floor((totalMinutes % (60 * 24)) / 60) - if (days > 0) return `~${days}d ${hours}h` - if (hours > 0) return `~${hours}h` - return `~${totalMinutes}m` -} - -export interface IssueUtilityNftsStepProps { - accountIndex: number - accountAddress: string | null - utxos: ScripthashUtxoEntry[] - esplora: EsploraClient - seedHex: string - preparedTxid: string | null - storedAuxiliaryAssetId?: string | null - prepareFirstVout?: number - currentBlockHeight?: number | null - /** When set, show read-only summary of completed step 1 (user returned to this step). */ - step1Summary?: Step1Summary | null - /** When set and step1Summary is null, fetch summary from Esplora by this txid (for recovery flow). */ - issuanceTxidForSummary?: string | null - onSuccess: (txid: string, summary: Step1Summary) => void - /** When user clicks "Start over" on the summary view. */ - onStartOver?: () => void -} - -export function IssueUtilityNftsStep({ - accountIndex, - accountAddress, - utxos, - esplora, - seedHex, - preparedTxid, - storedAuxiliaryAssetId, - prepareFirstVout = 0, - currentBlockHeight = null, - step1Summary = null, - issuanceTxidForSummary = null, - onSuccess, - onStartOver, -}: IssueUtilityNftsStepProps) { - const prepareVoutList = useMemo(() => prepareVouts(prepareFirstVout), [prepareFirstVout]) - - const nativeUtxos = useMemo(() => { - const policyId = POLICY_ASSET_ID[P2PK_NETWORK] - return utxos.filter((u) => !u.asset || u.asset.trim().toLowerCase() === policyId) - }, [utxos]) - - const auxiliaryAssetIdNorm = (storedAuxiliaryAssetId ?? '').trim().toLowerCase() || null - const auxiliaryUtxos = useMemo(() => { - if (!auxiliaryAssetIdNorm) return [] - return utxos.filter((u) => u.asset && u.asset.trim().toLowerCase() === auxiliaryAssetIdNorm) - }, [utxos, auxiliaryAssetIdNorm]) - - const effectivePrepTxid = (preparedTxid ?? '').trim() || null - - const issuanceUtxosOrdered = useMemo(() => { - if (!effectivePrepTxid) return null - const prepTxidLower = effectivePrepTxid.toLowerCase() - const byVout: Record = {} - for (const u of auxiliaryUtxos) { - if (u.txid.trim().toLowerCase() === prepTxidLower && prepareVoutList.includes(u.vout)) { - byVout[u.vout] = u - } - } - const list = prepareVoutList.map((v) => byVout[v]).filter(Boolean) - return list.length === 4 - ? (list as [ - ScripthashUtxoEntry, - ScripthashUtxoEntry, - ScripthashUtxoEntry, - ScripthashUtxoEntry, - ]) - : null - }, [auxiliaryUtxos, effectivePrepTxid, prepareVoutList]) - - const { missingVouts } = useMemo(() => { - if (!effectivePrepTxid) return { missingVouts: [...prepareVoutList] } - const prepTxidLower = effectivePrepTxid.toLowerCase() - const found = new Set() - for (const u of utxos) { - if (u.txid.trim().toLowerCase() === prepTxidLower && prepareVoutList.includes(u.vout)) { - found.add(u.vout) - } - } - const missingVouts = prepareVoutList.filter((v) => !found.has(v)) - return { missingVouts } - }, [utxos, effectivePrepTxid, prepareVoutList]) - - const someMissing = Boolean(effectivePrepTxid && missingVouts.length > 0) - - const [feeUtxoIndex, setFeeUtxoIndex] = useState(0) - const [feeAmount, setFeeAmount] = useState('') - const [collateralAmount, setCollateralAmount] = useState('') - const [principalAmount, setPrincipalAmount] = useState('') - const [loanExpirationTime, setLoanExpirationTime] = useState('') - const [interestPercent, setInterestPercent] = useState('') - const [building, setBuilding] = useState(false) - const [buildError, setBuildError] = useState(null) - const [builtIssueTx, setBuiltIssueTx] = useState - > | null>(null) - const [broadcastError, setBroadcastError] = useState(null) - const [signedTxHex, setSignedTxHex] = useState(null) - const [fetchedSummary, setFetchedSummary] = useState(null) - const [summaryFetchError, setSummaryFetchError] = useState(null) - const [loadingSummary, setLoadingSummary] = useState(false) - - const bottomAnchorRef = useRef(null) - useEffect(() => { - if (buildError || broadcastError || builtIssueTx || signedTxHex) { - bottomAnchorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) - } - }, [buildError, broadcastError, builtIssueTx, signedTxHex]) - - const loanExpirationNum = parseInt(loanExpirationTime, 10) || 0 - - useEffect(() => { - const txid = (issuanceTxidForSummary ?? '').trim() - if (!txid || step1Summary != null) { - setFetchedSummary(null) - setSummaryFetchError(null) - setLoadingSummary(false) - return - } - let cancelled = false - setLoadingSummary(true) - setSummaryFetchError(null) - esplora - .getTx(txid) - .then((tx) => { - if (cancelled) return - const vouts = tx.vout ?? [] - const v2 = vouts[2] - const v3 = vouts[3] - if (!v2?.value || v2.value < 0 || !v3?.value || v3.value < 0) { - setSummaryFetchError('Transaction missing or invalid Parameter NFT outputs.') - setFetchedSummary(null) - setLoadingSummary(false) - return - } - const first = decodeFirstNFTParameters(BigInt(v2.value)) - const second = decodeSecondNFTParameters(BigInt(v3.value)) - const collateralDec = first.collateralDec - const principalDec = first.principalDec - const collateralSat = second.collateralBaseAmount * 10 ** collateralDec - const principalSat = second.principalBaseAmount * 10 ** principalDec - setFetchedSummary({ - txid, - collateralAmount: String(collateralSat), - principalAmount: String(principalSat), - feeAmount: '—', - loanExpirationTime: String(first.loanExpirationTime), - interestPercent: (first.interestRateBasisPoints / 100).toFixed(2), - toAddress: '', - }) - setLoadingSummary(false) - }) - .catch((e) => { - if (!cancelled) { - setSummaryFetchError( - e instanceof EsploraApiError - ? (e.body ?? e.message) - : e instanceof Error - ? e.message - : String(e) - ) - setFetchedSummary(null) - setLoadingSummary(false) - } - }) - return () => { - cancelled = true - } - }, [esplora, issuanceTxidForSummary, step1Summary]) - - const endsInLabel = useMemo( - () => formatEndsIn(loanExpirationNum, currentBlockHeight ?? null), - [loanExpirationNum, currentBlockHeight] - ) - - const runBuild = useCallback( - async (broadcast: boolean) => { - if (!issuanceUtxosOrdered || !effectivePrepTxid || !seedHex) { - setBuildError('Missing issuance UTXOs or seed') - return - } - const issuanceEntropyBytes = crypto.getRandomValues(new Uint8Array(32)) - - const feeNum = parseInt(feeAmount, 10) || 0 - const collateralNum = BigInt(collateralAmount || '0') - const principalNum = BigInt(principalAmount || '0') - const feeUtxo = nativeUtxos[feeUtxoIndex] - if (!feeUtxo || feeNum <= 0 || (feeUtxo.value ?? 0) < feeNum) { - setBuildError('Invalid fee UTXO or amount') - return - } - - setBuildError(null) - setBroadcastError(null) - setBuilding(true) - try { - const tx = await esplora.getTx(effectivePrepTxid) - const prevouts = prepareVoutList.map((v) => tx.vout?.[v]).filter(Boolean) - if (prevouts.length !== 4) { - setBuildError('Could not load prevouts for prepare tx') - return - } - - const issuanceUtxos: [ - { outpoint: { txid: string; vout: number }; prevout: (typeof tx.vout)[0] }, - { outpoint: { txid: string; vout: number }; prevout: (typeof tx.vout)[0] }, - { outpoint: { txid: string; vout: number }; prevout: (typeof tx.vout)[0] }, - { outpoint: { txid: string; vout: number }; prevout: (typeof tx.vout)[0] }, - ] = [ - { - outpoint: { txid: effectivePrepTxid, vout: prepareVoutList[0]! }, - prevout: prevouts[0]!, - }, - { - outpoint: { txid: effectivePrepTxid, vout: prepareVoutList[1]! }, - prevout: prevouts[1]!, - }, - { - outpoint: { txid: effectivePrepTxid, vout: prepareVoutList[2]! }, - prevout: prevouts[2]!, - }, - { - outpoint: { txid: effectivePrepTxid, vout: prepareVoutList[3]! }, - prevout: prevouts[3]!, - }, - ] - - const feeTx = await esplora.getTx(feeUtxo.txid) - const feePrevout = feeTx.vout?.[feeUtxo.vout] - if (!feePrevout) { - setBuildError('Fee UTXO prevout not found') - return - } - - const firstAmount = encodeFirstNFTParameters( - percentToBasisPoints(parseFloat(interestPercent || '0')), - loanExpirationNum, - TOKEN_DECIMALS, - TOKEN_DECIMALS - ) - const secondAmount = encodeSecondNFTParameters( - toBaseAmount(collateralNum, TOKEN_DECIMALS), - toBaseAmount(principalNum, TOKEN_DECIMALS) - ) - - const built = await buildIssueUtilityNftsTx({ - issuanceUtxos, - feeUtxo: { outpoint: { txid: feeUtxo.txid, vout: feeUtxo.vout }, prevout: feePrevout }, - issuanceEntropyBytes, - firstParametersNftAmount: firstAmount, - secondParametersNftAmount: secondAmount, - utilityNftsToAddress: (accountAddress ?? '').trim(), - feeAmount: BigInt(feeNum), - network: P2PK_NETWORK, - }) - setBuiltIssueTx(built) - - if (broadcast) { - const seed = parseSeedHex(seedHex) - const secret = deriveSecretKeyFromIndex(seed, accountIndex) - const signedTxHex = await finalizeIssueUtilityNftsTx({ - pset: built.pset as PsetWithExtractTx, - prevouts: built.prevouts, - secretKey: secret, - network: P2PK_NETWORK, - }) - setSignedTxHex(signedTxHex) - const txidRes = await esplora.broadcastTx(signedTxHex) - onSuccess(txidRes, { - txid: txidRes, - collateralAmount, - principalAmount, - feeAmount, - loanExpirationTime, - interestPercent, - toAddress: accountAddress ?? '', - }) - } - } catch (e) { - if (e instanceof EsploraApiError) { - setBroadcastError(formatBroadcastError(e.body ?? e.message)) - } else { - setBuildError(e instanceof Error ? e.message : String(e)) - } - } finally { - setBuilding(false) - } - }, - [ - issuanceUtxosOrdered, - effectivePrepTxid, - prepareVoutList, - seedHex, - feeAmount, - collateralAmount, - principalAmount, - interestPercent, - loanExpirationNum, - loanExpirationTime, - accountAddress, - feeUtxoIndex, - nativeUtxos, - accountIndex, - esplora, - onSuccess, - ] - ) - - const handleSign = useCallback(async () => { - if (!builtIssueTx || !seedHex) return - setBuildError(null) - setBuilding(true) - try { - const seed = parseSeedHex(seedHex) - const secret = deriveSecretKeyFromIndex(seed, accountIndex) - const signedTxHex = await finalizeIssueUtilityNftsTx({ - pset: builtIssueTx.pset as PsetWithExtractTx, - prevouts: builtIssueTx.prevouts, - secretKey: secret, - network: P2PK_NETWORK, - }) - setSignedTxHex(signedTxHex) - } catch (e) { - setBuildError(e instanceof Error ? e.message : String(e)) - } finally { - setBuilding(false) - } - }, [builtIssueTx, seedHex, accountIndex]) - - const handleSignAndBroadcast = useCallback(async () => { - if (!builtIssueTx || !seedHex) return - setBuildError(null) - setBroadcastError(null) - setBuilding(true) - try { - const seed = parseSeedHex(seedHex) - const secret = deriveSecretKeyFromIndex(seed, accountIndex) - const signedTxHex = await finalizeIssueUtilityNftsTx({ - pset: builtIssueTx.pset as PsetWithExtractTx, - prevouts: builtIssueTx.prevouts, - secretKey: secret, - network: P2PK_NETWORK, - }) - setSignedTxHex(signedTxHex) - const txidRes = await esplora.broadcastTx(signedTxHex) - onSuccess(txidRes, { - txid: txidRes, - collateralAmount, - principalAmount, - feeAmount, - loanExpirationTime, - interestPercent, - toAddress: accountAddress ?? '', - }) - } catch (e) { - if (e instanceof EsploraApiError) { - setBroadcastError(formatBroadcastError(e.body ?? e.message)) - } else { - setBuildError(e instanceof Error ? e.message : String(e)) - } - } finally { - setBuilding(false) - } - }, [ - builtIssueTx, - seedHex, - accountIndex, - collateralAmount, - principalAmount, - feeAmount, - loanExpirationTime, - interestPercent, - accountAddress, - esplora, - onSuccess, - ]) - - const handleBuild = () => runBuild(false) - const handleBuildAndBroadcast = () => void handleSignAndBroadcast() - - const canBuild = - issuanceUtxosOrdered != null && - nativeUtxos.length > 0 && - feeAmount.trim() !== '' && - (accountAddress ?? '').trim() !== '' && - collateralAmount.trim() !== '' && - principalAmount.trim() !== '' && - loanExpirationTime.trim() !== '' - - const aprPercent = parseFloat(interestPercent || '0') - const principalNum = parseFloat(principalAmount || '0') || 0 - const totalRepay = principalNum * (1 + aprPercent / 100) - - const displaySummary = step1Summary ?? fetchedSummary - if (displaySummary) { - const txidShort = - displaySummary.txid.length <= 20 - ? displaySummary.txid - : `${displaySummary.txid.slice(0, 10)}…${displaySummary.txid.slice(-8)}` - return ( -
-
-

Issue Utility NFTs — summary

-

Transaction

-
- - {txidShort} - - -
-

Offer parameters used

- - - - - - - - - - - - - - - - - - - -
Collateral{displaySummary.collateralAmount} sat LBTC
Borrow{displaySummary.principalAmount} sat ASSET
Duration (block) - {(() => { - const blockNum = parseInt(displaySummary.loanExpirationTime, 10) - const endsIn = - !Number.isNaN(blockNum) && currentBlockHeight != null - ? formatEndsIn(blockNum, currentBlockHeight) - : '' - return endsIn - ? `block ${displaySummary.loanExpirationTime} (${endsIn})` - : `block ${displaySummary.loanExpirationTime}` - })()} -
Interest{displaySummary.interestPercent}%
-
- {onStartOver && ( -
- - Start flow from the beginning - -
- )} -
- ) - } - if (issuanceTxidForSummary?.trim() && loadingSummary) { - return ( -
-
- Loading summary… -
-
- ) - } - if (issuanceTxidForSummary?.trim() && summaryFetchError) { - return ( -
-
-

Could not load summary

-

{summaryFetchError}

-
- {onStartOver && ( -
- - Start flow from the beginning - -
- )} -
- ) - } - - return ( -
-
- {someMissing && ( -

- Not all 4 UTXOs from the prepare tx were found (missing vouts: {missingVouts.join(', ')} - ). -

- )} - -

Offer parameters

-
-
- - setCollateralAmount(e.target.value)} - suffix="sat LBTC" - /> -
-
- - setPrincipalAmount(e.target.value)} - suffix="sat ASSET" - /> -
-
-
- - {currentBlockHeight != null && ( - - )} -
-
- setLoanExpirationTime(e.target.value)} - /> - {endsInLabel &&

Ends in {endsInLabel}

} -
-
-
- - { - const raw = e.target.value.replace(',', '.') - const m = raw.match(/^\d*\.?\d{0,2}/) - setInterestPercent(m ? m[0] : interestPercent) - }} - /> -
-
- -
-

Transaction details

-
-
- - {nativeUtxos.length === 0 ? ( -

No LBTC UTXOs.

- ) : ( - setFeeUtxoIndex(parseInt(v, 10))} - optionValueType="index" - labelSuffix="sats" - /> - )} -
-
- - setFeeAmount(e.target.value)} - suffix="sat LBTC" - /> -
-
-
- -
-

Offer summary

- - - - - - - - - - - - - - - - - - - -
Collateral{collateralAmount || '0'} sat LBTC
Borrow{principalAmount || '0'} sat ASSET
Offer ends - block {loanExpirationTime || '—'} - {endsInLabel && ` (${endsInLabel})`} -
Total to repay - {totalRepay.toLocaleString(undefined, { maximumFractionDigits: 2 })} sat ASSET - {principalAmount && interestPercent && ( - - (principal + {interestPercent}% interest) - - )} -
-
-
- void handleSign()} - onSignAndBroadcast={handleBuildAndBroadcast} - broadcastButtonLabel="Sign & Broadcast" - canBuild={canBuild} - thirdButtonRequiresOnlyBuilt - /> -
- - - -
- ) -} diff --git a/web/src/pages/CreateOffer/PrepareStep.tsx b/web/src/pages/CreateOffer/PrepareStep.tsx deleted file mode 100644 index 4a4fddd..0000000 --- a/web/src/pages/CreateOffer/PrepareStep.tsx +++ /dev/null @@ -1,300 +0,0 @@ -/** - * Step 1: Prepare 4 UTXOs for Utility NFTs issuance. - * One LBTC input with issuance → 4×10 of new asset to address + change + fee. - */ - -import { useCallback, useMemo, useState, useRef, useEffect } from 'react' -import { parseSeedHex, deriveSecretKeyFromIndex } from '../../utility/seed' -import { P2PK_NETWORK, POLICY_ASSET_ID } from '../../utility/addressP2pk' -import { EsploraApiError, type EsploraClient } from '../../api/esplora' -import { formatBroadcastError } from '../../utils/parseBroadcastError' -import type { ScripthashUtxoEntry } from '../../api/esplora' -import type { PsetWithExtractTx } from '../../simplicity' -import { - buildPrepareUtilityNftsTx, - finalizePrepareUtilityNftsTx, -} from '../../tx/prepareUtilityNfts/buildPrepareUtilityNftsTx' -import { BroadcastStatusContent } from '../../components/PostBroadcastModal' -import { TxActionButtons } from '../../components/TxActionButtons' -import { TxStatusBlock } from '../../components/TxStatusBlock' -import { getBroadcastSuccessMessage } from '../../components/broadcastSuccessMessages' -import { CopyIcon } from '../../components/CopyIcon' -import { ButtonSecondary, ButtonIconNeutral } from '../../components/Button' -import { Input } from '../../components/Input' -import { UtxoSelect } from '../../components/UtxoSelect' -import { formClassNames } from '../../components/formClassNames' - -export interface PrepareStepProps { - accountIndex: number - accountAddress: string | null - utxos: ScripthashUtxoEntry[] - esplora: EsploraClient - seedHex: string - existingPreparedTxid?: string | null - onClearSavedPrepare?: () => void - onSuccess: (txid: string, auxiliaryAssetId?: string, issuanceEntropyHex?: string) => void -} - -export function PrepareStep({ - accountIndex, - accountAddress, - utxos, - esplora, - seedHex, - existingPreparedTxid, - onClearSavedPrepare, - onSuccess, -}: PrepareStepProps) { - const nativeUtxos = useMemo(() => { - const policyId = POLICY_ASSET_ID[P2PK_NETWORK] - return utxos.filter((u) => !u.asset || u.asset.trim().toLowerCase() === policyId) - }, [utxos]) - - const [selectedUtxoIndex, setSelectedUtxoIndex] = useState(0) - const [feeAmount, setFeeAmount] = useState('') - const [toAddress, setToAddress] = useState(accountAddress ?? '') - const [building, setBuilding] = useState(false) - const [buildError, setBuildError] = useState(null) - const [builtPrepareTx, setBuiltPrepareTx] = useState - > | null>(null) - const [signedTxHex, setSignedTxHex] = useState(null) - const [broadcastTxid, setBroadcastTxid] = useState(null) - const [broadcastError, setBroadcastError] = useState(null) - - const bottomAnchorRef = useRef(null) - useEffect(() => { - if (buildError || broadcastError || signedTxHex) { - bottomAnchorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) - } - }, [buildError, broadcastError, signedTxHex]) - - const selectedUtxo = - nativeUtxos.length > 0 && selectedUtxoIndex >= 0 && selectedUtxoIndex < nativeUtxos.length - ? nativeUtxos[selectedUtxoIndex] - : null - - const feeNum = parseInt(feeAmount, 10) || 0 - const minFeeUtxoValue = feeNum - const canBuild = - selectedUtxo != null && - (selectedUtxo.value ?? 0) >= minFeeUtxoValue && - feeNum > 0 && - toAddress.trim().length > 0 - - const handleBuild = useCallback(async () => { - if (!selectedUtxo || !canBuild) return - setBuildError(null) - setBuiltPrepareTx(null) - setSignedTxHex(null) - setBroadcastTxid(null) - setBroadcastError(null) - setBuilding(true) - try { - const tx = await esplora.getTx(selectedUtxo.txid) - const prevout = tx.vout?.[selectedUtxo.vout] - if (!prevout) { - setBuildError(`No output at index ${selectedUtxo.vout}`) - return - } - if (prevout.value == null) { - setBuildError('Output is confidential (value not exposed)') - return - } - const result = await buildPrepareUtilityNftsTx({ - feeUtxo: { - outpoint: { txid: selectedUtxo.txid, vout: selectedUtxo.vout }, - prevout, - }, - toAddress: toAddress.trim(), - feeAmount: BigInt(feeNum), - network: P2PK_NETWORK, - }) - setBuiltPrepareTx(result) - } catch (e) { - setBuildError(e instanceof Error ? e.message : String(e)) - } finally { - setBuilding(false) - } - }, [selectedUtxo, canBuild, toAddress, feeNum, esplora]) - - const handleSign = useCallback(async () => { - if (!builtPrepareTx || !seedHex) return - setBuildError(null) - setBuilding(true) - try { - const seed = parseSeedHex(seedHex) - const secret = deriveSecretKeyFromIndex(seed, accountIndex) - const hex = await finalizePrepareUtilityNftsTx({ - pset: builtPrepareTx.pset as PsetWithExtractTx, - prevouts: builtPrepareTx.prevouts, - secretKey: secret, - network: P2PK_NETWORK, - }) - setSignedTxHex(hex) - } catch (e) { - setBuildError(e instanceof Error ? e.message : String(e)) - } finally { - setBuilding(false) - } - }, [builtPrepareTx, seedHex, accountIndex]) - - const handleBuildAndBroadcast = useCallback(async () => { - if (!builtPrepareTx || !seedHex || !selectedUtxo || !canBuild) return - setBuildError(null) - setSignedTxHex(null) - setBroadcastTxid(null) - setBroadcastError(null) - setBuilding(true) - try { - const seed = parseSeedHex(seedHex) - const secret = deriveSecretKeyFromIndex(seed, accountIndex) - const hex = await finalizePrepareUtilityNftsTx({ - pset: builtPrepareTx.pset as PsetWithExtractTx, - prevouts: builtPrepareTx.prevouts, - secretKey: secret, - network: P2PK_NETWORK, - }) - const txidRes = await esplora.broadcastTx(hex) - setBroadcastTxid(txidRes) - setSignedTxHex(hex) - } catch (e) { - if (e instanceof EsploraApiError) { - setBroadcastError(formatBroadcastError(e.body ?? e.message)) - setBroadcastTxid(null) - } else { - setBuildError(e instanceof Error ? e.message : String(e)) - } - } finally { - setBuilding(false) - } - }, [builtPrepareTx, seedHex, accountIndex, selectedUtxo, canBuild, esplora]) - - const showAlreadyPrepared = Boolean(existingPreparedTxid?.trim()) - - const handlePostBroadcastClose = () => { - onSuccess(broadcastTxid!, builtPrepareTx?.auxiliaryAssetId, builtPrepareTx?.issuanceEntropyHex) - setBroadcastTxid(null) - } - - if (broadcastTxid) { - return ( -
- -
- ) - } - - return ( -
-

Step 1: Prepare 4 UTXOs

-
- {showAlreadyPrepared && ( -
-

Already prepared for this account

-

- Use Step 2; the 4 issuance UTXOs are vouts 0, 1, 2, 3 of this transaction. -

-

- - {existingPreparedTxid} - - navigator.clipboard?.writeText(existingPreparedTxid!)} - title="Copy txid" - aria-label="Copy txid" - > - - -

- - Prepare again - -
- )} - - {!showAlreadyPrepared && ( - <> -
-

Fee UTXO (LBTC)

- {nativeUtxos.length === 0 ? ( -

- No LBTC UTXOs in your account. Use Utility to create some. -

- ) : ( - setSelectedUtxoIndex(parseInt(v, 10))} - optionValueType="index" - labelSuffix="sats" - /> - )} -
- -
-

Fee amount (sats)

- setFeeAmount(e.target.value)} - /> -
- -
-

To address (4×10 new asset + change)

- setToAddress(e.target.value)} - /> - {accountAddress && ( -

- Default: current account. Fee UTXO must have at least {minFeeUtxoValue} sats - (fee). Prep creates 4×10 of a new asset to this address. -

- )} -
- -
- -
- - - -
- ) -} diff --git a/web/src/pages/CreateOffer/RepaymentModal.tsx b/web/src/pages/CreateOffer/RepaymentModal.tsx deleted file mode 100644 index 877ebcf..0000000 --- a/web/src/pages/CreateOffer/RepaymentModal.tsx +++ /dev/null @@ -1,536 +0,0 @@ -/** - * Modal to repay an active loan (borrower): spend Lending UTXO + 3 NFTs + principal UTXO(s) + fee, - * get collateral back. Opens when user clicks an active offer in YOUR BORROWS. - */ - -import { useMemo, useState, useEffect, useRef } from 'react' -import { Modal } from '../../components/Modal' -import { TxActionButtons } from '../../components/TxActionButtons' -import { TxStatusBlock } from '../../components/TxStatusBlock' -import { BroadcastStatusContent } from '../../components/PostBroadcastModal' -import { getBroadcastSuccessMessage } from '../../components/broadcastSuccessMessages' -import { InfoTooltip } from '../../components/InfoTooltip' -import { Input } from '../../components/Input' -import { UtxoSelect } from '../../components/UtxoSelect' -import { formClassNames } from '../../components/formClassNames' -import type { OfferShort } from '../../types/offers' -import type { ScripthashUtxoEntry } from '../../api/esplora' -import type { EsploraClient } from '../../api/esplora' -import { EsploraApiError } from '../../api/esplora' -import { formatBroadcastError } from '../../utils/parseBroadcastError' -import { - fetchOfferUtxos, - fetchOfferParticipantsHistory, - getCurrentBorrowerParticipant, -} from '../../api/client' -import { P2PK_NETWORK, POLICY_ASSET_ID } from '../../utility/addressP2pk' -import { getScriptPubkeyHexFromAddress } from '../../utility/addressP2pk' -import { buildLoanRepaymentTx } from '../../tx/loanRepayment/buildLoanRepaymentTx' -import { finalizeLoanRepaymentTx } from '../../tx/loanRepayment/finalizeLoanRepaymentTx' -import { calculatePrincipalWithInterest } from '../../tx/loanRepayment/principalWithInterest' -import { parseSeedHex, deriveSecretKeyFromIndex } from '../../utility/seed' -import type { PsetWithExtractTx } from '../../simplicity' -import type { BuildLoanRepaymentTxResult } from '../../tx/loanRepayment/buildLoanRepaymentTx' -import type { LoanRepaymentPrincipalUtxo } from '../../tx/loanRepayment/buildLoanRepaymentTx' -import { normalizeHex } from '../../utility/hex' - -function shortId(id: string, headLen = 8, tailLen = 4): string { - if (!id || id.length <= headLen + tailLen) return id - return `${id.slice(0, headLen)}…${id.slice(-tailLen)}` -} - -function formatSats(amount: bigint | number): string { - const n = Number(amount) - if (Number.isSafeInteger(n)) return n.toLocaleString() - return String(amount).replace(/\B(?=(\d{3})+(?!\d))/g, ',') -} - -export interface RepaymentModalProps { - offer: OfferShort - utxos: ScripthashUtxoEntry[] - esplora: EsploraClient - open: boolean - onClose: () => void - accountAddress: string | null - seedHex?: string | null - accountIndex?: number - onSuccess?: () => void -} - -export function RepaymentModal({ - offer, - utxos, - esplora, - open, - onClose, - accountAddress, - seedHex: seedHexProp = null, - accountIndex = 0, - onSuccess, -}: RepaymentModalProps) { - const [offerUtxos, setOfferUtxos] = useState> | null>( - null - ) - const [offerUtxosLoading, setOfferUtxosLoading] = useState(false) - const [offerUtxosError, setOfferUtxosError] = useState(null) - const [participantsHistory, setParticipantsHistory] = useState - > | null>(null) - const [selectedPrincipalIndices, setSelectedPrincipalIndices] = useState([]) - const [feeUtxoIndex, setFeeUtxoIndex] = useState(0) - const [feeAmount, setFeeAmount] = useState('500') - const [collateralDestinationAddress, setCollateralDestinationAddress] = useState('') - const [building, setBuilding] = useState(false) - const [buildError, setBuildError] = useState(null) - const [builtTx, setBuiltTx] = useState(null) - const [unsignedTxHex, setUnsignedTxHex] = useState(null) - const [signedTxHex, setSignedTxHex] = useState(null) - const [broadcastTxid, setBroadcastTxid] = useState(null) - - const principalAssetNorm = useMemo( - () => normalizeHex(offer.principal_asset), - [offer.principal_asset] - ) - const principalUtxos = useMemo(() => { - return utxos.filter((u) => { - const a = normalizeHex(u.asset ?? '') - return a === principalAssetNorm - }) - }, [utxos, principalAssetNorm]) - - const nativeUtxos = useMemo(() => { - const policyId = POLICY_ASSET_ID[P2PK_NETWORK] - return utxos.filter((u) => !u.asset || u.asset.trim().toLowerCase() === policyId) - }, [utxos]) - - const lendingUtxo = useMemo(() => { - if (!offerUtxos) return null - const u = offerUtxos.find((o) => o.utxo_type === 'lending' && o.spent_txid == null) - return u ?? null - }, [offerUtxos]) - - const currentBorrowerParticipant = useMemo(() => { - if (!participantsHistory) return null - return getCurrentBorrowerParticipant(participantsHistory) - }, [participantsHistory]) - - const principalWithInterest = useMemo( - () => calculatePrincipalWithInterest(offer.principal_amount, offer.interest_rate), - [offer.principal_amount, offer.interest_rate] - ) - const selectedPrincipalSum = useMemo(() => { - return selectedPrincipalIndices.reduce((sum, idx) => { - const u = principalUtxos[idx] - if (!u) return sum - const v = u.value - return sum + (typeof v === 'number' && v >= 0 ? BigInt(v) : 0n) - }, 0n) - }, [selectedPrincipalIndices, principalUtxos]) - const principalSufficient = selectedPrincipalSum >= principalWithInterest - - useEffect(() => { - if (open && offer.id) { - setOfferUtxos(null) - setOfferUtxosError(null) - setParticipantsHistory(null) - setOfferUtxosLoading(true) - setSelectedPrincipalIndices([]) - setBuildError(null) - setBuiltTx(null) - setUnsignedTxHex(null) - setSignedTxHex(null) - setBroadcastTxid(null) - setCollateralDestinationAddress(accountAddress ?? '') - Promise.all([fetchOfferUtxos(offer.id), fetchOfferParticipantsHistory(offer.id)]) - .then(([utxosRes, participants]) => { - setOfferUtxos(utxosRes) - setParticipantsHistory(participants) - }) - .catch((e) => { - setOfferUtxosError(e instanceof Error ? e.message : String(e)) - setOfferUtxos([]) - }) - .finally(() => setOfferUtxosLoading(false)) - } - }, [open, offer.id, accountAddress]) - - const togglePrincipalIndex = (idx: number) => { - setSelectedPrincipalIndices((prev) => - prev.includes(idx) ? prev.filter((i) => i !== idx) : [...prev, idx].sort((a, b) => a - b) - ) - } - - const handleBuild = async () => { - if (!lendingUtxo || !collateralDestinationAddress.trim()) { - setBuildError('Collateral destination address is required.') - return - } - if (!currentBorrowerParticipant) { - setBuildError( - 'Current Borrower NFT not found. It may already be spent or the offer was not accepted.' - ) - return - } - if (selectedPrincipalIndices.length === 0) { - setBuildError('Select at least one principal UTXO.') - return - } - if (!principalSufficient) { - setBuildError( - `Selected principal sum ${selectedPrincipalSum} is less than principal+interest ${principalWithInterest}.` - ) - return - } - - const feeEntry = nativeUtxos[feeUtxoIndex] - if (!feeEntry) { - setBuildError('Select a fee UTXO (LBTC).') - return - } - - const feeNum = parseInt(feeAmount, 10) || 0 - if (feeNum <= 0) { - setBuildError('Fee amount must be at least 1.') - return - } - - setBuildError(null) - setBuiltTx(null) - setUnsignedTxHex(null) - setSignedTxHex(null) - setBroadcastTxid(null) - setBuilding(true) - try { - const [lendingTx, borrowerNftTx, feeTx, ...principalTxs] = await Promise.all([ - esplora.getTx(lendingUtxo.txid), - esplora.getTx(currentBorrowerParticipant.txid), - esplora.getTx(feeEntry.txid), - ...selectedPrincipalIndices.map((idx) => { - const u = principalUtxos[idx] - if (!u) throw new Error('Principal UTXO entry missing') - return esplora.getTx(u.txid) - }), - ]) - const borrowerNftPrevout = borrowerNftTx.vout?.[currentBorrowerParticipant.vout] - if (!borrowerNftPrevout) throw new Error('Borrower NFT prevout not found.') - const feePrevout = feeTx.vout?.[feeEntry.vout] - if (!feePrevout) throw new Error('Fee UTXO prevout not found.') - - const principalUtxoParams: LoanRepaymentPrincipalUtxo[] = selectedPrincipalIndices.map( - (idx, i) => { - const u = principalUtxos[idx] - if (!u) throw new Error('Principal UTXO entry missing') - const tx = principalTxs[i] - if (!tx) throw new Error('Principal tx missing') - const prevout = tx.vout?.[u.vout] - if (!prevout) throw new Error(`Principal prevout not found for ${u.txid}:${u.vout}`) - return { - outpoint: { txid: u.txid, vout: u.vout }, - prevout, - } - } - ) - - const collateralScriptHex = await getScriptPubkeyHexFromAddress( - collateralDestinationAddress.trim() - ) - const principalChangeScriptHex = accountAddress - ? await getScriptPubkeyHexFromAddress(accountAddress.trim()) - : collateralScriptHex - - const result = await buildLoanRepaymentTx({ - lendingTx, - borrowerNftUtxo: { - outpoint: { - txid: currentBorrowerParticipant.txid, - vout: currentBorrowerParticipant.vout, - }, - prevout: borrowerNftPrevout, - }, - principalUtxos: principalUtxoParams, - feeUtxo: { - outpoint: { txid: feeEntry.txid, vout: feeEntry.vout }, - prevout: feePrevout, - }, - feeAmount: BigInt(feeNum), - collateralOutputScriptHex: collateralScriptHex, - principalChangeScriptHex, - offer, - network: P2PK_NETWORK, - }) - - setBuiltTx(result) - setUnsignedTxHex(result.unsignedTxHex) - setSignedTxHex(null) - } catch (e) { - setBuildError(e instanceof Error ? e.message : String(e)) - } finally { - setBuilding(false) - } - } - - const handleSign = async () => { - if (!builtTx) { - setBuildError('Build transaction first.') - return - } - if (!seedHexProp) { - setBuildError('Seed is required to sign.') - return - } - - setBuildError(null) - setBuilding(true) - try { - const secretKey = deriveSecretKeyFromIndex(parseSeedHex(seedHexProp), accountIndex) - const signed = await finalizeLoanRepaymentTx({ - pset: builtTx.pset as PsetWithExtractTx, - prevouts: builtTx.prevouts, - lendingCovHash: builtTx.lendingCovHash, - lendingArgs: builtTx.lendingArgs, - network: P2PK_NETWORK, - borrowerSecretKey: secretKey, - }) - setSignedTxHex(signed.signedTxHex) - setBroadcastTxid(null) - } catch (e) { - setBuildError(e instanceof Error ? e.message : String(e)) - } finally { - setBuilding(false) - } - } - - const handleRepay = async () => { - if (!signedTxHex) { - setBuildError('Sign transaction first.') - return - } - setBuildError(null) - setBuilding(true) - try { - const txid = await esplora.broadcastTx(signedTxHex) - setBroadcastTxid(txid) - } catch (e) { - if (e instanceof EsploraApiError) { - setBuildError(formatBroadcastError(e.body ?? e.message)) - } else { - setBuildError(e instanceof Error ? e.message : String(e)) - } - } finally { - setBuilding(false) - } - } - - const principalLabel = shortId(offer.principal_asset, 4).toUpperCase() - const collateralExplorerUrl = esplora.getAssetExplorerUrl(offer.collateral_asset) - - const handleClose = () => { - if (broadcastTxid) { - onSuccess?.() - } - setBroadcastTxid(null) - onClose() - } - - const bottomAnchorRef = useRef(null) - useEffect(() => { - if (buildError || unsignedTxHex || signedTxHex) { - bottomAnchorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) - } - }, [buildError, unsignedTxHex, signedTxHex]) - - return ( - - {broadcastTxid ? ( - - ) : ( -
-

- Repay borrowed principal token and unlock the collateral. -

- - {offerUtxosLoading &&

Loading offer UTXOs…

} - {offerUtxosError && ( -

{offerUtxosError}

- )} - {!offerUtxosLoading && offerUtxos != null && !lendingUtxo && ( -

- No unspent Lending UTXO for this offer. It may not be accepted yet or already - repaid/liquidated. -

- )} - - {lendingUtxo && participantsHistory != null && !currentBorrowerParticipant && ( -

- Current Borrower NFT not found. It may already be spent or the data is not yet - indexed. -

- )} - - {lendingUtxo && currentBorrowerParticipant && ( - <> -
-

Lending UTXO

-

- {shortId(lendingUtxo.txid, 10)} : {lendingUtxo.vout} -

-

- Borrower NFT: {shortId(currentBorrowerParticipant.txid, 10)} : - {currentBorrowerParticipant.vout} -

-

- Collateral: {formatSats(offer.collateral_amount)} sats ( - - {shortId(offer.collateral_asset, 8)} - - ) -

-
- -
-
-

- Collateral destination -

- -
- setCollateralDestinationAddress(e.target.value)} - /> -
- -
-
-

- Principal UTXOs ({principalLabel}) -

- -
- {principalUtxos.length === 0 ? ( -

No UTXOs in principal asset.

- ) : ( -
- {principalUtxos.map((u, idx) => { - const value = - typeof u.value === 'number' && u.value >= 0 ? BigInt(u.value) : 0n - const selected = selectedPrincipalIndices.includes(idx) - const disableCheckbox = principalSufficient && !selected - return ( - - ) - })} -
- )} - {selectedPrincipalIndices.length > 0 && ( -

- Sum: {formatSats(selectedPrincipalSum)} — need{' '} - {formatSats(principalWithInterest)}{' '} - {principalSufficient ? ( - ✓ - ) : ( - (insufficient) - )} -

- )} -
- -
-
-

- Transaction details -

- -
-
-
-

Fee UTXO (LBTC)

- {nativeUtxos.length === 0 ? ( -

No LBTC UTXOs.

- ) : ( - setFeeUtxoIndex(parseInt(v, 10))} - optionValueType="index" - labelSuffix="sats" - /> - )} -
-
-

Fee amount (sats)

- setFeeAmount(e.target.value)} - /> -
-
-
- - void handleBuild()} - onSign={() => void handleSign()} - onSignAndBroadcast={() => void handleRepay()} - broadcastButtonLabel="Repay" - canBuild={ - nativeUtxos.length > 0 && principalUtxos.length > 0 && !!principalSufficient - } - /> - - - - )} - - ) -} diff --git a/web/src/pages/CreateOffer/borrowerStorage.ts b/web/src/pages/CreateOffer/borrowerStorage.ts deleted file mode 100644 index e44adc1..0000000 --- a/web/src/pages/CreateOffer/borrowerStorage.ts +++ /dev/null @@ -1,71 +0,0 @@ -const BORROWER_PREPARE_TXID_KEY = 'simplicity-lending-borrower-prepare-txid' -const BORROWER_AUXILIARY_ASSET_ID_KEY = 'simplicity-lending-borrower-auxiliary-asset-id' -const BORROWER_PREPARE_FIRST_VOUT_KEY = 'simplicity-lending-borrower-prepare-first-vout' -const BORROWER_ISSUANCE_TXID_KEY = 'simplicity-lending-borrower-issuance-txid' - -export function getStoredPrepareTxid(accountIndex: number): string | null { - if (typeof localStorage === 'undefined') return null - const raw = localStorage.getItem(`${BORROWER_PREPARE_TXID_KEY}-${accountIndex}`) - return raw ?? null -} - -export function savePrepareTxid(accountIndex: number, txid: string): void { - if (typeof localStorage === 'undefined') return - localStorage.setItem(`${BORROWER_PREPARE_TXID_KEY}-${accountIndex}`, txid) -} - -export function clearStoredPrepareTxid(accountIndex: number): void { - if (typeof localStorage === 'undefined') return - localStorage.removeItem(`${BORROWER_PREPARE_TXID_KEY}-${accountIndex}`) -} - -export function getStoredAuxiliaryAssetId(accountIndex: number): string | null { - if (typeof localStorage === 'undefined') return null - const raw = localStorage.getItem(`${BORROWER_AUXILIARY_ASSET_ID_KEY}-${accountIndex}`) - return raw ?? null -} - -export function saveStoredAuxiliaryAssetId(accountIndex: number, assetId: string): void { - if (typeof localStorage === 'undefined') return - localStorage.setItem(`${BORROWER_AUXILIARY_ASSET_ID_KEY}-${accountIndex}`, assetId.trim()) -} - -export function clearStoredAuxiliaryAssetId(accountIndex: number): void { - if (typeof localStorage === 'undefined') return - localStorage.removeItem(`${BORROWER_AUXILIARY_ASSET_ID_KEY}-${accountIndex}`) -} - -/** First vout of the 4 prepare UTXOs (0 when created in-app, user-set when imported). */ -export function getStoredPrepareFirstVout(accountIndex: number): number { - if (typeof localStorage === 'undefined') return 0 - const raw = localStorage.getItem(`${BORROWER_PREPARE_FIRST_VOUT_KEY}-${accountIndex}`) - if (raw == null) return 0 - const n = parseInt(raw, 10) - return Number.isFinite(n) && n >= 0 ? n : 0 -} - -export function saveStoredPrepareFirstVout(accountIndex: number, firstVout: number): void { - if (typeof localStorage === 'undefined') return - localStorage.setItem(`${BORROWER_PREPARE_FIRST_VOUT_KEY}-${accountIndex}`, String(firstVout)) -} - -export function clearStoredPrepareFirstVout(accountIndex: number): void { - if (typeof localStorage === 'undefined') return - localStorage.removeItem(`${BORROWER_PREPARE_FIRST_VOUT_KEY}-${accountIndex}`) -} - -export function getStoredIssuanceTxid(accountIndex: number): string | null { - if (typeof localStorage === 'undefined') return null - const raw = localStorage.getItem(`${BORROWER_ISSUANCE_TXID_KEY}-${accountIndex}`) - return raw ?? null -} - -export function saveStoredIssuanceTxid(accountIndex: number, txid: string): void { - if (typeof localStorage === 'undefined') return - localStorage.setItem(`${BORROWER_ISSUANCE_TXID_KEY}-${accountIndex}`, txid.trim()) -} - -export function clearStoredIssuanceTxid(accountIndex: number): void { - if (typeof localStorage === 'undefined') return - localStorage.removeItem(`${BORROWER_ISSUANCE_TXID_KEY}-${accountIndex}`) -} diff --git a/web/src/pages/CreateOffer/index.tsx b/web/src/pages/CreateOffer/index.tsx deleted file mode 100644 index bb27d34..0000000 --- a/web/src/pages/CreateOffer/index.tsx +++ /dev/null @@ -1,781 +0,0 @@ -/** - * Borrower page: dashboard and Create Borrow Offer wizard in a popup. - * Uses useAccountAddress for UTXOs and refresh after broadcast. - * Offers are loaded from Indexer API (by-script + batch). - */ - -import { useCallback, useMemo, useState, useEffect } from 'react' -import type { Tab } from '../../App' -import { useSeedHex } from '../../SeedContext' -import { useAccountAddress } from '../../hooks/useAccountAddress' -import { - fetchOfferIdsByScript, - fetchOfferIdsByBorrowerPubkey, - fetchOfferDetailsBatchWithParticipants, - filterOffersByParticipantRole, -} from '../../api/client' -import { EsploraClient, EsploraApiError } from '../../api/esplora' -import { - getScriptPubkeyHexFromAddress, - getP2pkAddressFromSecret, - P2PK_NETWORK, -} from '../../utility/addressP2pk' -import { parseSeedHex, deriveSecretKeyFromIndex } from '../../utility/seed' -import { CreateOfferWizard } from './CreateOfferWizard' -import { RepaymentModal } from './RepaymentModal' -import { CancelOfferModal } from './CancelOfferModal' -import { PrepareStep } from './PrepareStep' -import { Modal } from '../../components/Modal' -import { Input } from '../../components/Input' -import { formClassNames } from '../../components/formClassNames' -import { OfferTable } from '../../components/OfferTable' -import { - getStoredPrepareTxid, - savePrepareTxid, - clearStoredPrepareTxid, - getStoredAuxiliaryAssetId, - saveStoredAuxiliaryAssetId, - clearStoredAuxiliaryAssetId, - getStoredPrepareFirstVout, - saveStoredPrepareFirstVout, - clearStoredPrepareFirstVout, - getStoredIssuanceTxid, - saveStoredIssuanceTxid, - clearStoredIssuanceTxid, -} from './borrowerStorage' -import { ISSUANCE_TX_FIRST_RETURN_VOUT } from '../../tx/issueUtilityNfts/buildIssueUtilityNftsTx' -import type { OfferShort } from '../../types/offers' - -function WalletIcon({ className }: { className?: string }) { - return ( - - - - - - ) -} - -function SettingsIcon({ className }: { className?: string }) { - return ( - - - - - ) -} - -/** Recover/restore icon for "Recover flow" button. */ -function RecoverIcon({ className }: { className?: string }) { - return ( - - - - - ) -} - -type PrepareTab = 'create' | 'import' - -function PrepareModalContent({ - accountIndex, - accountAddress, - utxos, - esplora, - seedHex, - onSuccess, - onClose, -}: { - accountIndex: number - accountAddress: string | null - utxos: import('../../api/esplora').ScripthashUtxoEntry[] - esplora: EsploraClient - seedHex: string - onSuccess: (txid: string, auxiliaryAssetId: string | undefined, firstVout: number) => void - onClose: () => void -}) { - const [tab, setTab] = useState('create') - const [importTxid, setImportTxid] = useState('') - const [importFirstVout, setImportFirstVout] = useState('0') - const [importError, setImportError] = useState(null) - const [importing, setImporting] = useState(false) - - const handleImport = useCallback(async () => { - const txid = importTxid.trim() - const firstVout = parseInt(importFirstVout, 10) - if (!txid || Number.isNaN(firstVout) || firstVout < 0) { - setImportError('Enter a valid txid and first vout (≥ 0).') - return - } - setImportError(null) - setImporting(true) - try { - const [tx, outspends] = await Promise.all([esplora.getTx(txid), esplora.getTxOutspends(txid)]) - const vouts = tx.vout ?? [] - for (let i = 0; i < 4; i++) { - const idx = firstVout + i - if (idx >= vouts.length) { - setImportError(`Output at index ${idx} does not exist (tx has ${vouts.length} outputs).`) - return - } - } - const v0 = vouts[firstVout] - const assetId = (v0?.asset ?? '').trim().toLowerCase() - if (!assetId) { - setImportError('First output has no asset.') - return - } - for (let i = 1; i < 4; i++) { - const v = vouts[firstVout + i] - const a = (v?.asset ?? '').trim().toLowerCase() - if (a !== assetId) { - setImportError(`Output at index ${firstVout + i} has different asset.`) - return - } - } - for (let i = 0; i < 4; i++) { - const o = outspends[firstVout + i] - if (o?.spent) { - setImportError(`Output at index ${firstVout + i} is already spent.`) - return - } - } - onSuccess(txid, assetId, firstVout) - } catch (e) { - setImportError( - e instanceof EsploraApiError ? e.message : e instanceof Error ? e.message : String(e) - ) - } finally { - setImporting(false) - } - }, [importTxid, importFirstVout, esplora, onSuccess]) - - return ( -
-

- Create 4 UTXOs of a single auxiliary asset for Utility NFTs, or import an existing prepare - by transaction ID and first output index. -

-
- - -
- {tab === 'create' ? ( - { - onSuccess(txid, auxiliaryAssetId, 0) - }} - /> - ) : ( -
-
- - setImportTxid(e.target.value)} - placeholder="txid hex" - className="w-full font-mono" - /> -
-
- - setImportFirstVout(e.target.value)} - className="w-full" - /> -
- {importError && ( -

- {importError} -

- )} -
- - -
-
- )} -
- ) -} - -export function CreateOfferPage({ - accountIndex, - onTab, -}: { - accountIndex: number - onTab: (t: Tab) => void -}) { - const seedHex = useSeedHex() - const esplora = useMemo(() => new EsploraClient(), []) - const { - address: accountAddress, - utxos, - loading, - error, - refresh, - } = useAccountAddress({ - seedHex, - accountIndex, - esplora, - }) - - const [showCreateWizard, setShowCreateWizard] = useState(false) - const [showPrepareModal, setShowPrepareModal] = useState(false) - const [showSettingsModal, setShowSettingsModal] = useState(false) - const [savedPreparedTxid, setSavedPreparedTxid] = useState(() => - getStoredPrepareTxid(accountIndex) - ) - const [savedAuxiliaryAssetId, setSavedAuxiliaryAssetId] = useState(() => - getStoredAuxiliaryAssetId(accountIndex) - ) - const [savedPrepareFirstVout, setSavedPrepareFirstVout] = useState(() => - getStoredPrepareFirstVout(accountIndex) - ) - const [savedIssuanceTxid, setSavedIssuanceTxid] = useState(() => - getStoredIssuanceTxid(accountIndex) - ) - const [prepareUtxosSpent, setPrepareUtxosSpent] = useState(null) - const [importIssuanceTxid, setImportIssuanceTxid] = useState('') - const [importIssuanceError, setImportIssuanceError] = useState(null) - const [importingIssuance, setImportingIssuance] = useState(false) - const [borrowOffers, setBorrowOffers] = useState([]) - const [offersLoading, setOffersLoading] = useState(true) - const [offersError, setOffersError] = useState(null) - const [currentBlockHeight, setCurrentBlockHeight] = useState(null) - const [repaymentOffer, setRepaymentOffer] = useState(null) - const [cancelOffer, setCancelOffer] = useState(null) - - const loadBorrowOffers = useCallback(async () => { - if (!accountAddress) { - setBorrowOffers([]) - setOffersLoading(false) - return - } - setOffersLoading(true) - setOffersError(null) - try { - const scriptPubkeyHex = await getScriptPubkeyHexFromAddress(accountAddress) - let idsByBorrowerPubkey: string[] = [] - if (seedHex) { - try { - const seed = parseSeedHex(seedHex) - const secretKey = deriveSecretKeyFromIndex(seed, accountIndex) - const { internalKeyHex } = await getP2pkAddressFromSecret(secretKey, P2PK_NETWORK) - idsByBorrowerPubkey = await fetchOfferIdsByBorrowerPubkey(internalKeyHex) - } catch { - idsByBorrowerPubkey = [] - } - } - const idsByScript = await fetchOfferIdsByScript(scriptPubkeyHex) - const allIds = [...new Set([...idsByScript, ...idsByBorrowerPubkey])] - const [withParticipants, height] = await Promise.all([ - allIds.length === 0 ? Promise.resolve([]) : fetchOfferDetailsBatchWithParticipants(allIds), - esplora.getLatestBlockHeight().catch(() => null), - ]) - const listByScript = filterOffersByParticipantRole( - withParticipants, - scriptPubkeyHex, - 'borrower' - ) - const setByScriptIds = new Set(listByScript.map((o) => o.id)) - const listPendingByPubkey = withParticipants.filter( - (o) => idsByBorrowerPubkey.includes(o.id) && !setByScriptIds.has(o.id) - ) - const listPendingAsShort: OfferShort[] = listPendingByPubkey.map((offerWithParticipants) => { - const { participants, ...offer } = offerWithParticipants - void participants - return offer - }) - const list = [...listByScript, ...listPendingAsShort] - setBorrowOffers(list) - setCurrentBlockHeight(height) - } catch (e) { - setOffersError(e instanceof Error ? e.message : String(e)) - setBorrowOffers([]) - } finally { - setOffersLoading(false) - } - }, [accountAddress, accountIndex, seedHex, esplora]) - - useEffect(() => { - setSavedPreparedTxid(getStoredPrepareTxid(accountIndex)) - setSavedAuxiliaryAssetId(getStoredAuxiliaryAssetId(accountIndex)) - setSavedPrepareFirstVout(getStoredPrepareFirstVout(accountIndex)) - setSavedIssuanceTxid(getStoredIssuanceTxid(accountIndex)) - }, [accountIndex]) - - useEffect(() => { - if (!savedPreparedTxid?.trim()) { - setPrepareUtxosSpent(null) - return - } - const first = savedPrepareFirstVout - let cancelled = false - esplora - .getTxOutspends(savedPreparedTxid) - .then((outspends) => { - if (cancelled) return - const indices = [first, first + 1, first + 2, first + 3] - const anySpent = indices.some((i) => outspends[i]?.spent === true) - setPrepareUtxosSpent(anySpent) - }) - .catch(() => { - if (!cancelled) setPrepareUtxosSpent(null) - }) - return () => { - cancelled = true - } - }, [esplora, savedPreparedTxid, savedPrepareFirstVout]) - - useEffect(() => { - loadBorrowOffers() - }, [loadBorrowOffers]) - - const hasPrepared = Boolean(savedPreparedTxid?.trim()) && Boolean(savedAuxiliaryAssetId?.trim()) - const canCreateOffer = hasPrepared || Boolean(savedIssuanceTxid?.trim()) - - const handleClearPrepare = useCallback(() => { - clearStoredPrepareTxid(accountIndex) - clearStoredAuxiliaryAssetId(accountIndex) - clearStoredPrepareFirstVout(accountIndex) - setSavedPreparedTxid(null) - setSavedAuxiliaryAssetId(null) - setSavedPrepareFirstVout(0) - setShowSettingsModal(false) - }, [accountIndex]) - - const handleIssueUtilityNftsSuccess = useCallback( - (txid: string) => { - saveStoredIssuanceTxid(accountIndex, txid) - setSavedIssuanceTxid(txid) - savePrepareTxid(accountIndex, txid) - saveStoredPrepareFirstVout(accountIndex, ISSUANCE_TX_FIRST_RETURN_VOUT) - setSavedPreparedTxid(txid) - setSavedPrepareFirstVout(ISSUANCE_TX_FIRST_RETURN_VOUT) - }, - [accountIndex] - ) - - const handlePrepareAgain = useCallback(() => { - handleClearPrepare() - setShowPrepareModal(true) - }, [handleClearPrepare]) - - const [wizardKey, setWizardKey] = useState(0) - const handleStartOver = useCallback(() => { - clearStoredIssuanceTxid(accountIndex) - setSavedIssuanceTxid(null) - setWizardKey((k) => k + 1) - }, [accountIndex]) - - /** After step 2 (Finalize offer) broadcast success: clear used issuance txid and reset wizard so next open is step 1. */ - const handleStep2Complete = useCallback(() => { - clearStoredIssuanceTxid(accountIndex) - setSavedIssuanceTxid(null) - setWizardKey((k) => k + 1) - setShowCreateWizard(false) - }, [accountIndex]) - - const handleRecoverTxid = useCallback(async () => { - const txid = importIssuanceTxid.trim() - if (!txid) { - setImportIssuanceError('Enter a txid.') - return - } - setImportIssuanceError(null) - setImportingIssuance(true) - try { - const tx = await esplora.getTx(txid) - if (!tx.vout || tx.vout.length < 4) { - setImportIssuanceError('Transaction must have at least 4 outputs (Utility NFTs).') - return - } - const outspends = await esplora.getTxOutspends(txid) - const nftVouts = [0, 1, 2, 3] - const anySpent = nftVouts.some((i) => outspends[i]?.spent === true) - if (anySpent) { - setImportIssuanceError( - "This transaction's outputs have already been spent. The flow cannot be continued." - ) - return - } - saveStoredIssuanceTxid(accountIndex, txid) - setSavedIssuanceTxid(txid) - setImportIssuanceTxid('') - setShowRecoverPanel(false) - } catch (e) { - setImportIssuanceError( - e instanceof EsploraApiError - ? (e.body ?? e.message) - : e instanceof Error - ? e.message - : String(e) - ) - } finally { - setImportingIssuance(false) - } - }, [accountIndex, esplora, importIssuanceTxid]) - - const [showRecoverPanel, setShowRecoverPanel] = useState(false) - - if (!seedHex) { - return

Connect seed to create an offer.

- } - - if (loading) { - return

Loading…

- } - - if (error) { - return

{error}

- } - - return ( -
- - -
-
-
- -

YOUR BORROWS

-
- -
- { - if (offer.status === 'active') { - setRepaymentOffer(offer) - } else if (offer.status === 'pending') { - setCancelOffer(offer) - } - }} - /> -
- - {hasPrepared && prepareUtxosSpent === true ? ( -
-

- One or more prepare UTXOs have already been spent. Please prepare again. -

- -
- ) : canCreateOffer ? ( - - ) : ( - - )} - - setShowPrepareModal(false)} - title="Prepare to be a borrower" - > - { - savePrepareTxid(accountIndex, txid) - saveStoredAuxiliaryAssetId(accountIndex, auxiliaryAssetId ?? '') - saveStoredPrepareFirstVout(accountIndex, firstVout) - setSavedPreparedTxid(txid) - setSavedAuxiliaryAssetId(auxiliaryAssetId ?? null) - setSavedPrepareFirstVout(firstVout) - setShowPrepareModal(false) - void refresh() - }} - onClose={() => setShowPrepareModal(false)} - /> - - - {repaymentOffer != null && ( - setRepaymentOffer(null)} - accountAddress={accountAddress} - seedHex={seedHex} - accountIndex={accountIndex} - onSuccess={() => { - loadBorrowOffers() - setRepaymentOffer(null) - }} - /> - )} - - {cancelOffer != null && ( - setCancelOffer(null)} - accountAddress={accountAddress} - seedHex={seedHex} - accountIndex={accountIndex} - onSuccess={() => { - loadBorrowOffers() - setCancelOffer(null) - }} - /> - )} - - setShowSettingsModal(false)} - title="Borrower settings" - > -
- {savedPreparedTxid ? ( - <> -
-

Prepare txid

-

{savedPreparedTxid}

-
- {savedAuxiliaryAssetId && ( -
-

Auxiliary asset id

-

- {savedAuxiliaryAssetId} -

-
- )} -
-

First vout

-

{savedPrepareFirstVout}

-
- - ) : ( -

- No prepare data. Use "Prepare to be a borrower" first. -

- )} -
- - -
-
-
- - setShowCreateWizard(false)} - title="Create Borrow Offer" - contentClassName="max-w-xl" - > - setShowRecoverPanel((v) => !v)} - className="rounded-xl border border-gray-300 bg-white p-2 text-gray-600 hover:bg-gray-50 hover:text-gray-700" - title="Recover" - aria-label="Recover" - > - - - } - recoveryPanel={ - showRecoverPanel ? ( -
-

- If you have an existing issuance txid (e.g. from another device), enter it to - continue. -

-
- { - setImportIssuanceTxid(e.target.value) - setImportIssuanceError(null) - }} - /> - - -
- {importIssuanceError && ( -

{importIssuanceError}

- )} -
- ) : null - } - /> -
-
- ) -} diff --git a/web/src/pages/Dashboard/index.tsx b/web/src/pages/Dashboard/index.tsx index 8e110ad..6a19f58 100644 --- a/web/src/pages/Dashboard/index.tsx +++ b/web/src/pages/Dashboard/index.tsx @@ -1,25 +1,19 @@ -/** - * Dashboard: YOUR BORROWS / YOUR SUPPLY cards and MOST RECENT 10 SUPPLY OFFERS table. - * Borrow button → Borrower tab; Supply button → Lender tab. - * Offers are loaded from Indexer API (GET /offers). - */ - import { useCallback, useEffect, useMemo, useState } from 'react' -import type { Tab } from '../../App' -import { useSeedHex } from '../../SeedContext' import { - fetchOfferIdsByBorrowerPubkey, - fetchOfferIdsByScript, fetchOfferDetailsBatchWithParticipants, + fetchOfferIdsByBorrowerPubkey, + fetchOfferIdsByScripts, fetchOffers, - filterOffersByParticipantRole, + filterOffersByParticipantScripts, } from '../../api/client' import { EsploraClient } from '../../api/esplora' import { OfferTable } from '../../components/OfferTable' import type { OfferShort } from '../../types/offers' -import { getScriptPubkeyHexFromAddress, getP2pkAddressFromSecret, P2PK_NETWORK } from '../../utility/addressP2pk' -import { POLICY_ASSET_ID } from '../../utility/addressP2pk' -import { deriveSecretKeyFromIndex, parseSeedHex } from '../../utility/seed' +import { mergeBorrowerOffers, summarizeBorrowerOffers } from '../../utility/borrowerOffers' +import type { WalletAbiNetwork } from 'wallet-abi-sdk-alpha/schema' +import { loadTrackedBorrowerOfferIds } from '../../walletAbi/borrowerTrackedStorage' +import { loadTrackedLenderOfferIds } from '../../walletAbi/lenderStorage' +import { loadKnownWalletScripts } from '../../walletAbi/walletScriptStorage' interface BorrowStats { lockedLbtc: bigint @@ -43,283 +37,255 @@ const ZERO_SUPPLY_STATS: SupplyStats = { waitingLiquidation: 0, } -function formatBigint(value: bigint): string { - const s = value.toString() - return s.replace(/\B(?=(\d{3})+(?!\d))/g, ',') +function formatSats(value: bigint): string { + const asNumber = Number(value) + if (Number.isSafeInteger(asNumber)) { + return asNumber.toLocaleString() + } + return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') } -function StatValue({ - loading, +function StatCard({ + label, value, - emphasize = false, + loading, + accent, }: { - loading: boolean + label: string value: string | number - emphasize?: boolean + loading: boolean + accent: string }) { - if (loading) { - return