diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2c53d44 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.DS_Store +target +web/node_modules +web/dist +deployment/configs/compose.env +web/.env 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/Cargo.lock b/Cargo.lock index c524862..ac782c8 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=eab94b8#eab94b8c3002db003079c1e1436434c58866f760" +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=eab94b8#eab94b8c3002db003079c1e1436434c58866f760" +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/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/Cargo.toml b/crates/contracts/Cargo.toml index 45e1695..dfdf88c 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 = "eab94b8", 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/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/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/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/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 +} 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_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..8c5e318 --- /dev/null +++ b/crates/contracts/tests/support/wallet_abi_lending_support.rs @@ -0,0 +1,2117 @@ +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?; + harness.mine_and_sync(1).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..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(), + 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, + }, + 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?; + 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..925d76b --- /dev/null +++ b/crates/contracts/tests/wallet_abi_asset_auth.rs @@ -0,0 +1,98 @@ +#[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)?; + harness.sync_wallet().await?; + + 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)?; + harness.sync_wallet().await?; + + 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)?; + harness.sync_wallet().await?; + + 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..c0fccd1 --- /dev/null +++ b/crates/contracts/tests/wallet_abi_script_auth.rs @@ -0,0 +1,96 @@ +#[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)?; + harness.sync_wallet().await?; + + 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)?; + harness.sync_wallet().await?; + + 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)?; + harness.sync_wallet().await?; + + 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(()) +} 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..5ae90f4 100644 --- a/crates/indexer/configuration/base.yaml +++ b/crates/indexer/configuration/base.yaml @@ -1,5 +1,8 @@ application: port: 8000 + cors: + allowed_origins: + - "*" database: host: "localhost" port: 5432 @@ -11,4 +14,4 @@ esplora: timeout: 10 indexer: interval: 10000 - last_indexed_height: 2309541 \ No newline at end of file + last_indexed_height: 2342450 diff --git a/crates/indexer/src/api/server.rs b/crates/indexer/src/api/server.rs index 19fbaee..916ac4e 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, + Json, Router, + http::{HeaderValue, Method, Uri}, routing::{get, post}, }; use sqlx::PgPool; use tokio::net::TcpListener; +use tokio::task::JoinSet; +use tower_http::cors::{AllowOrigin, Any, CorsLayer}; use tower_http::request_id::{self, MakeRequestUuid, RequestId}; use tower_http::trace::TraceLayer; @@ -19,10 +22,58 @@ 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 parse_allowed_origins(origins: &[String]) -> Result>, String> { + let normalized = origins + .iter() + .map(|origin| origin.trim()) + .filter(|origin| !origin.is_empty()) + .collect::>(); + + if normalized.is_empty() || normalized.contains(&"*") { + return Ok(None); + } + + let mut parsed = Vec::with_capacity(normalized.len()); + + for origin in normalized { + origin + .parse::() + .map_err(|error| format!("`{origin}` is not a valid URI: {error}"))?; + + let header = HeaderValue::from_str(origin) + .map_err(|error| format!("`{origin}` is not a valid header value: {error}"))?; - let app = Router::new() + parsed.push(header); + } + + Ok(Some(parsed)) +} + +fn build_cors_layer(allowed_origins: &[String]) -> io::Result { + let base = CorsLayer::new() + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_headers(Any); + + let parsed_origins = parse_allowed_origins(allowed_origins).map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid CORS origin in configuration: {error}"), + ) + })?; + + Ok(match parsed_origins { + None => base.allow_origin(Any), + Some(origins) => base.allow_origin(AllowOrigin::list(origins)), + }) +} + +async fn healthcheck() -> Json { + Json(serde_json::json!({ "status": "ok" })) +} + +fn build_app(state: Arc, cors: CorsLayer) -> Router { + Router::new() + .route("/health", get(healthcheck)) .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 +93,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 +111,134 @@ 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, + allowed_origins: &[String], +) -> io::Result<()> { + let state = Arc::new(AppState { db: db_pool }); + let cors = build_cors_layer(allowed_origins)?; + let app = build_app(state, cors); + 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, parse_allowed_origins}; + + #[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"] + ); + } + + #[test] + fn custom_host_is_preserved() { + assert_eq!( + listener_bind_targets("192.168.1.50", 9000), + vec!["192.168.1.50:9000"] + ); + } + + #[test] + fn empty_cors_origins_defaults_to_wildcard() { + assert_eq!(parse_allowed_origins(&[]).unwrap(), None); + } + + #[test] + fn wildcard_cors_origin_defaults_to_any() { + assert_eq!(parse_allowed_origins(&["*".into()]).unwrap(), None); + } + + #[test] + fn explicit_cors_origins_are_preserved() { + let origins = parse_allowed_origins(&[ + "https://app.example.com".into(), + "https://admin.example.com".into(), + ]) + .unwrap() + .unwrap(); + + assert_eq!(origins.len(), 2); + assert_eq!(origins[0], "https://app.example.com"); + assert_eq!(origins[1], "https://admin.example.com"); + } - axum::serve(listener, app).await.unwrap() + #[test] + fn invalid_cors_origin_is_rejected() { + assert!(parse_allowed_origins(&["https://bad origin.example.com".into()]).is_err()); + } } diff --git a/crates/indexer/src/configuration.rs b/crates/indexer/src/configuration.rs index 690a5a3..959d33d 100644 --- a/crates/indexer/src/configuration.rs +++ b/crates/indexer/src/configuration.rs @@ -10,6 +10,26 @@ pub struct Settings { pub struct ApplicationSettings { pub port: u16, pub host: String, + #[serde(default)] + pub cors: CorsSettings, +} + +#[derive(serde::Deserialize, Clone)] +pub struct CorsSettings { + #[serde(default = "default_cors_allowed_origins")] + pub allowed_origins: Vec, +} + +impl Default for CorsSettings { + fn default() -> Self { + Self { + allowed_origins: default_cors_allowed_origins(), + } + } +} + +fn default_cors_allowed_origins() -> Vec { + vec!["*".to_string()] } #[derive(serde::Deserialize, Clone)] diff --git a/crates/indexer/src/db.rs b/crates/indexer/src/db.rs index 6a6e540..967b222 100644 --- a/crates/indexer/src/db.rs +++ b/crates/indexer/src/db.rs @@ -1,3 +1,9 @@ -use sqlx::{Postgres, Transaction}; +use sqlx::{PgPool, Postgres, Transaction, migrate::Migrator}; pub type DbTx<'a> = Transaction<'a, Postgres>; + +static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); + +pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> { + MIGRATOR.run(pool).await +} diff --git a/crates/indexer/src/main.rs b/crates/indexer/src/main.rs index a5bbed3..462a8bb 100644 --- a/crates/indexer/src/main.rs +++ b/crates/indexer/src/main.rs @@ -1,8 +1,7 @@ use lending_indexer::esplora_client::EsploraClient; use lending_indexer::telemetry::{get_subscriber, init_subscriber}; -use lending_indexer::{api, indexer}; +use lending_indexer::{api, db, indexer}; use sqlx::PgPool; -use tokio::net::TcpListener; use lending_indexer::configuration::get_configuration; @@ -12,27 +11,57 @@ async fn main() -> Result<(), std::io::Error> { init_subscriber(subscriber); let configuration = get_configuration().expect("Failed to read configuration"); - let pool = PgPool::connect_lazy(&configuration.database.connection_string()) - .expect("Failed to connect to Postgres."); + let database_connection_string = configuration.database.connection_string(); let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "api".into()); match run_mode.as_str() { + "migrate" => { + tracing::info!("Running database migrations"); + let pool = PgPool::connect(&database_connection_string) + .await + .map_err(|error| { + std::io::Error::other(format!("Failed to connect to Postgres: {error}")) + })?; + + db::run_migrations(&pool).await.map_err(|error| { + std::io::Error::other(format!("Failed to run migrations: {error}")) + })?; + + tracing::info!("Database migrations completed"); + } "indexer" => { + let pool = PgPool::connect_lazy(&database_connection_string).map_err(|error| { + std::io::Error::other(format!("Failed to connect to Postgres: {error}")) + })?; 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 pool = PgPool::connect_lazy(&database_connection_string).map_err(|error| { + std::io::Error::other(format!("Failed to connect to Postgres: {error}")) + })?; + 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, + &configuration.application.cors.allowed_origins, + ) + .await?; } } diff --git a/deployment/Dockerfile.backend b/deployment/Dockerfile.backend new file mode 100644 index 0000000..e8f2262 --- /dev/null +++ b/deployment/Dockerfile.backend @@ -0,0 +1,24 @@ +FROM rust:1.90-bookworm AS builder + +WORKDIR /app + +COPY Cargo.toml Cargo.lock ./ +COPY .sqlx ./.sqlx +COPY crates ./crates + +RUN SQLX_OFFLINE=true cargo build --locked --release -p lending-indexer --bin simplicity-lending-indexer + +FROM debian:bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates gettext-base \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app/crates/indexer + +COPY --from=builder /app/target/release/simplicity-lending-indexer /usr/local/bin/simplicity-lending-indexer +COPY deployment/configs /deployment/configs +COPY deployment/scripts/render-backend-config.sh /deployment/render-backend-config.sh + +ENTRYPOINT ["/deployment/render-backend-config.sh"] +CMD ["simplicity-lending-indexer"] diff --git a/deployment/Dockerfile.web b/deployment/Dockerfile.web new file mode 100644 index 0000000..77dd75d --- /dev/null +++ b/deployment/Dockerfile.web @@ -0,0 +1,26 @@ +FROM node:24-bookworm AS builder + +WORKDIR /app/web + +COPY web/package.json web/package-lock.json ./ +COPY web/vendor ./vendor + +RUN npm install --no-audit --no-fund + +COPY web/.prettierignore web/.prettierrc web/eslint.config.js web/index.html web/postcss.config.js web/simplicity-covenants.config.json web/tailwind.config.js web/tsconfig.app.json web/tsconfig.json web/tsconfig.node.json web/vite.config.ts ./ +COPY web/public ./public +COPY web/src ./src +COPY crates/contracts/src /app/crates/contracts/src + +RUN npm run build + +FROM nginx:1.27-alpine + +RUN apk add --no-cache gettext + +COPY --from=builder /app/web/dist /usr/share/nginx/html +COPY deployment/configs /deployment/configs +COPY deployment/scripts/render-web-config.sh /deployment/render-web-config.sh + +ENTRYPOINT ["/deployment/render-web-config.sh"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000..71eedd9 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,99 @@ +# Dockerized Simplicity Lending Deployment + +This deployment stack runs: + +- `postgres` for indexed state +- `migrate` to apply the SQLx schema before the app starts +- `api` for the lending protocol HTTP API +- `indexer` for background chain indexing +- `web` for the public frontend, including `/api` and `/esplora` reverse proxies + +## Layout + +Most deployment knobs live in [`deployment/configs`](/Users/inter/Desktop/Simpl/simplicity-lending/deployment/configs): + +- `compose.env.example`: the single env file template for compose and runtime rendering +- `backend/*.template`: rendered into the indexer `configuration/` directory +- `nginx/default.conf.template`: reverse proxy and SPA serving rules +- `web/runtime-config.js.template`: browser runtime config injected at container start + +## Quick Start + +From [`deployment`](/Users/inter/Desktop/Simpl/simplicity-lending/deployment): + +```bash +cp ./configs/compose.env.example ./configs/compose.env +docker compose --env-file ./configs/compose.env -f docker-compose.yml up --build -d +``` + +The stack publishes only the web container on `WEB_PORT`. The browser calls: + +- `/api/*` -> internal `api:8000` +- `/esplora/*` -> `${WEB_ESPLORA_API_UPSTREAM}` + +That same-origin shape avoids browser CORS issues for normal deployment. + +## Required Configuration + +Set these values in [`deployment/configs/compose.env`](/Users/inter/Desktop/Simpl/simplicity-lending/deployment/configs/compose.env): + +- `PUBLIC_ORIGIN`: the final public HTTPS origin, for example `https://lending.example.com` +- `WEB_PORT`: host port mapped to the public web server +- `REOWN_PROJECT_ID`: the Reown / WalletConnect project id used by the browser +- `WALLET_ABI_NETWORK`: `liquid`, `testnet-liquid`, or `localtest-liquid` +- `API_CORS_ALLOWED_ORIGINS`: a YAML inline list, for example `["https://lending.example.com"]` +- `INDEXER_ESPLORA_BASE_URL`: the Esplora API used by the backend indexer +- `WEB_ESPLORA_API_UPSTREAM`: the Esplora API proxied by nginx, for example `https://blockstream.info/liquidtestnet/api` +- `WEB_ESPLORA_EXPLORER_URL`: the explorer UI base used for transaction links, for example `https://blockstream.info/liquidtestnet` +- `INDEXER_POLL_INTERVAL_MS`: background polling interval +- `INDEXER_LAST_INDEXED_HEIGHT`: first height to index from +- `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`: database connection settings + +## WalletConnect / Reown Notes + +WalletConnect is browser-originated. Your server does not terminate WalletConnect sessions or relay traffic. The web app serves the JavaScript, and the browser talks directly to Reown / WalletConnect infrastructure. + +Important deployment constraints: + +- `PUBLIC_ORIGIN` should be the exact HTTPS origin users open in the browser. +- That same origin must be allowlisted in your Reown project settings. +- Wallet metadata is built from `window.location.origin`, so the externally visible host and scheme must be correct at the edge. +- The app now uses `/vite.svg` as the WalletConnect metadata icon, so that asset must stay publicly reachable. +- Corporate proxies, VPNs, privacy extensions, or firewalls can block the WalletConnect relay. If pairing fails, test browser access without those layers first. +- WalletConnect depends on secure browser context behavior. Treat plain HTTP as local-dev only. + +Official references: + +- [Reown AppKit Installation](https://docs.reown.com/appkit/javascript/core/installation) +- [Reown AppKit FAQ](https://docs.reown.com/appkit/faq) +- [WalletConnect Relay](https://docs.walletconnect.network/wallet-sdk/web/cloud/relay) + +## CORS, Proxying, and Other Deployment Pitfalls + +- The default deployment keeps the API private and exposes it through nginx as `/api`. In that shape, browser CORS is mostly avoided. +- The backend still has explicit CORS configuration. Keep `API_CORS_ALLOWED_ORIGINS` aligned with `PUBLIC_ORIGIN`. +- If you expose the API on a separate origin, you must widen `API_CORS_ALLOWED_ORIGINS` and set the frontend runtime config to use that different API base instead of `/api`. +- The frontend also makes browser-side Esplora calls. Keeping `/esplora` on the same origin avoids a second CORS surface. +- This deployment assumes the app is served from a domain root, not a subpath. If you need a subpath deployment, you will need additional Vite base-path work. + +## Useful Commands + +From [`deployment`](/Users/inter/Desktop/Simpl/simplicity-lending/deployment): + +```bash +docker compose --env-file ./configs/compose.env -f docker-compose.yml config +docker compose --env-file ./configs/compose.env -f docker-compose.yml build +docker compose --env-file ./configs/compose.env -f docker-compose.yml up -d +docker compose --env-file ./configs/compose.env -f docker-compose.yml logs -f web api indexer +docker compose --env-file ./configs/compose.env -f docker-compose.yml down +``` + +## Smoke Checks + +Once the stack is running, verify: + +- `http://localhost:${WEB_PORT}/` +- `http://localhost:${WEB_PORT}/runtime-config.js` +- `http://localhost:${WEB_PORT}/api/health` +- `http://localhost:${WEB_PORT}/api/offers?limit=1` +- `http://localhost:${WEB_PORT}/esplora/blocks/tip/height` diff --git a/deployment/configs/.gitignore b/deployment/configs/.gitignore new file mode 100644 index 0000000..3d17dd3 --- /dev/null +++ b/deployment/configs/.gitignore @@ -0,0 +1 @@ +compose.env diff --git a/deployment/configs/backend/base.yaml.template b/deployment/configs/backend/base.yaml.template new file mode 100644 index 0000000..3923861 --- /dev/null +++ b/deployment/configs/backend/base.yaml.template @@ -0,0 +1,16 @@ +application: + port: 8000 + cors: + allowed_origins: ${API_CORS_ALLOWED_ORIGINS} +database: + host: "${POSTGRES_HOST}" + port: ${POSTGRES_PORT} + username: "${POSTGRES_USER}" + password: "${POSTGRES_PASSWORD}" + database_name: "${POSTGRES_DB}" +esplora: + base_url: "${INDEXER_ESPLORA_BASE_URL}" + timeout: 10 +indexer: + interval: ${INDEXER_POLL_INTERVAL_MS} + last_indexed_height: ${INDEXER_LAST_INDEXED_HEIGHT} diff --git a/deployment/configs/backend/production.yaml.template b/deployment/configs/backend/production.yaml.template new file mode 100644 index 0000000..b936a88 --- /dev/null +++ b/deployment/configs/backend/production.yaml.template @@ -0,0 +1,2 @@ +application: + host: 0.0.0.0 diff --git a/deployment/configs/compose.env.example b/deployment/configs/compose.env.example new file mode 100644 index 0000000..88f1c05 --- /dev/null +++ b/deployment/configs/compose.env.example @@ -0,0 +1,19 @@ +PUBLIC_ORIGIN=https://lending.example.com +WEB_PORT=8080 + +REOWN_PROJECT_ID=your_reown_project_id +WALLET_ABI_NETWORK=testnet-liquid + +API_CORS_ALLOWED_ORIGINS=["https://lending.example.com"] + +INDEXER_ESPLORA_BASE_URL=https://blockstream.info/liquidtestnet/api +WEB_ESPLORA_API_UPSTREAM=https://blockstream.info/liquidtestnet/api +WEB_ESPLORA_EXPLORER_URL=https://blockstream.info/liquidtestnet +INDEXER_POLL_INTERVAL_MS=10000 +INDEXER_LAST_INDEXED_HEIGHT=2342450 + +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=lending-indexer +POSTGRES_USER=app +POSTGRES_PASSWORD=secret diff --git a/deployment/configs/nginx/default.conf.template b/deployment/configs/nginx/default.conf.template new file mode 100644 index 0000000..3351b24 --- /dev/null +++ b/deployment/configs/nginx/default.conf.template @@ -0,0 +1,39 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location = /api { + return 301 /api/; + } + + location /api/ { + proxy_pass http://api:8000/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /esplora { + return 301 /esplora/; + } + + location /esplora/ { + proxy_pass ${WEB_ESPLORA_API_UPSTREAM}/; + proxy_http_version 1.1; + proxy_redirect off; + } + + location = /runtime-config.js { + add_header Cache-Control "no-store"; + try_files /runtime-config.js =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/deployment/configs/web/runtime-config.js.template b/deployment/configs/web/runtime-config.js.template new file mode 100644 index 0000000..99764dc --- /dev/null +++ b/deployment/configs/web/runtime-config.js.template @@ -0,0 +1,7 @@ +window.__SIMPLICITY_LENDING_CONFIG__ = { + apiBaseUrl: '/api', + reownProjectId: '${REOWN_PROJECT_ID}', + walletAbiNetwork: '${WALLET_ABI_NETWORK}', + esploraBaseUrl: '/esplora', + esploraExplorerUrl: '${WEB_ESPLORA_EXPLORER_URL}', +} diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml new file mode 100644 index 0000000..a620c4e --- /dev/null +++ b/deployment/docker-compose.yml @@ -0,0 +1,75 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + env_file: + - ./configs/compose.env + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"'] + interval: 5s + timeout: 5s + retries: 12 + + migrate: + build: + context: .. + dockerfile: deployment/Dockerfile.backend + restart: 'no' + env_file: + - ./configs/compose.env + environment: + APP_ENVIRONMENT: production + RUN_MODE: migrate + depends_on: + postgres: + condition: service_healthy + + api: + build: + context: .. + dockerfile: deployment/Dockerfile.backend + restart: unless-stopped + env_file: + - ./configs/compose.env + environment: + APP_ENVIRONMENT: production + RUN_MODE: api + depends_on: + migrate: + condition: service_completed_successfully + + indexer: + build: + context: .. + dockerfile: deployment/Dockerfile.backend + restart: unless-stopped + env_file: + - ./configs/compose.env + environment: + APP_ENVIRONMENT: production + RUN_MODE: indexer + depends_on: + migrate: + condition: service_completed_successfully + + web: + build: + context: .. + dockerfile: deployment/Dockerfile.web + restart: unless-stopped + env_file: + - ./configs/compose.env + ports: + - '${WEB_PORT}:80' + depends_on: + api: + condition: service_started + +volumes: + postgres_data: diff --git a/deployment/scripts/render-backend-config.sh b/deployment/scripts/render-backend-config.sh new file mode 100755 index 0000000..56fc909 --- /dev/null +++ b/deployment/scripts/render-backend-config.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +CONFIG_DIR="/app/crates/indexer/configuration" + +mkdir -p "$CONFIG_DIR" + +envsubst < /deployment/configs/backend/base.yaml.template > "$CONFIG_DIR/base.yaml" +envsubst < /deployment/configs/backend/production.yaml.template > "$CONFIG_DIR/production.yaml" + +exec "$@" diff --git a/deployment/scripts/render-web-config.sh b/deployment/scripts/render-web-config.sh new file mode 100755 index 0000000..c1ad81b --- /dev/null +++ b/deployment/scripts/render-web-config.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +envsubst '${WEB_ESPLORA_API_UPSTREAM}' \ + < /deployment/configs/nginx/default.conf.template \ + > /etc/nginx/conf.d/default.conf + +envsubst '${REOWN_PROJECT_ID} ${WALLET_ABI_NETWORK} ${WEB_ESPLORA_EXPLORER_URL}' \ + < /deployment/configs/web/runtime-config.js.template \ + > /usr/share/nginx/html/runtime-config.js + +exec "$@" 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..f436791 100644 --- a/web/README.md +++ b/web/README.md @@ -1,14 +1,24 @@ # 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. +> The web app now vendors its `lwk_web` and `wallet-abi-sdk-alpha` package dependencies inside [`web/vendor`](/Users/inter/Desktop/Simpl/simplicity-lending/web/vendor), so Docker builds and local installs no longer depend on sibling repositories. + +## 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 @@ -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/index.html b/web/index.html index 48f5a74..3f66b01 100644 --- a/web/index.html +++ b/web/index.html @@ -8,6 +8,7 @@
+ diff --git a/web/package-lock.json b/web/package-lock.json index bd01367..97139aa 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:./vendor/lwk_web", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "wallet-abi-sdk-alpha": "file:./vendor/wallet-abi-sdk-alpha" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -43,8 +50,42 @@ "../../../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", + "extraneous": true, "license": "MIT OR BSD-2-Clause" }, + "../../wallet-abi-sdk": { + "name": "wallet-abi-sdk-alpha", + "version": "0.2.0", + "extraneous": true, + "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", @@ -58,6 +99,114 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@andrewbranch/untar.js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@andrewbranch/untar.js/-/untar.js-1.0.3.tgz", + "integrity": "sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==", + "dev": true + }, + "node_modules/@arethetypeswrong/cli": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.18.2.tgz", + "integrity": "sha512-PcFM20JNlevEDKBg4Re29Rtv2xvjvQZzg7ENnrWFSS0PHgdP2njibVFw+dRUhNkPgNfac9iUqO0ohAXqQL4hbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@arethetypeswrong/core": "0.18.2", + "chalk": "^4.1.2", + "cli-table3": "^0.6.3", + "commander": "^10.0.1", + "marked": "^9.1.2", + "marked-terminal": "^7.1.0", + "semver": "^7.5.4" + }, + "bin": { + "attw": "dist/index.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@arethetypeswrong/cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@arethetypeswrong/cli/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@arethetypeswrong/core": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.18.2.tgz", + "integrity": "sha512-GiwTmBFOU1/+UVNqqCGzFJYfBXEytUkiI+iRZ6Qx7KmUVtLm00sYySkfe203C9QtPG11yOz1ZaMek8dT/xnlgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@andrewbranch/untar.js": "^1.0.3", + "@loaderkit/resolve": "^1.0.2", + "cjs-module-lexer": "^1.2.3", + "fflate": "^0.8.2", + "lru-cache": "^11.0.1", + "semver": "^7.5.4", + "typescript": "5.6.1-rc", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@arethetypeswrong/core/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==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@arethetypeswrong/core/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.6.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.1-rc.tgz", + "integrity": "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -89,6 +238,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 +442,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 +500,85 @@ "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/@braidai/lang": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", + "integrity": "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==", + "dev": true, + "license": "ISC" + }, + "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/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -825,15 +1064,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -866,20 +1105,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -903,9 +1142,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -1041,6 +1280,352 @@ "@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/@loaderkit/resolve": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@loaderkit/resolve/-/resolve-1.0.4.tgz", + "integrity": "sha512-rJzYKVcV4dxJv+vW6jlvagF8zvGxHJ2+HTr1e2qOejfmGhAApgJHl8Aog4mMszxceTRiKTTbnpgmTO1bEZHV/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@braidai/lang": "^1.0.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/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "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/@publint/pack": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.4.tgz", + "integrity": "sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, + "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,9 +1983,1212 @@ "win32" ] }, - "node_modules/@tailwindcss/node": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz", + "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": { + "@safe-global/safe-apps-sdk": "^9.1.0", + "events": "^3.3.0" + } + }, + "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, + "dependencies": { + "@safe-global/safe-gateway-typescript-sdk": "^3.5.3", + "viem": "^2.1.1" + } + }, + "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, + "engines": { + "node": ">=16" + } + }, + "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", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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", + "engines": { + "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/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "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", @@ -1621,6 +3409,70 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz", @@ -1714,6 +3566,26 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/bun": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.10.tgz", + "integrity": "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.10" + } + }, + "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 +3604,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 +3614,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,18 +3631,41 @@ "@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", - "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -1782,7 +3678,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", + "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1798,16 +3694,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", - "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -1823,14 +3720,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", - "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -1845,14 +3742,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", - "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1863,9 +3760,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", - "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -1880,15 +3777,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", - "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -1905,9 +3802,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -1919,18 +3816,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", - "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -1946,27 +3843,40 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1986,16 +3896,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", - "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2010,13 +3920,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", - "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2028,9 +3938,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2144,15 +4054,443 @@ "tinyrainbow": "^1.2.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "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": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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": { + "tslib": "1.14.1" + } + }, + "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": { + "@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://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": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2170,10 +4508,23 @@ "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", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2187,11 +4538,35 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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 +4578,45 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "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/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2220,6 +4634,32 @@ "node": ">=12" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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 +4697,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 +4717,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 +4753,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", @@ -2285,6 +4822,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -2305,6 +4855,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2319,6 +4870,49 @@ "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/bun-types": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.10.tgz", + "integrity": "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2329,6 +4923,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 +4947,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", @@ -2384,31 +5001,200 @@ "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "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/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" }, "engines": { - "node": ">=10" + "node": "10.* || >= 12.*" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "optionalDependencies": { + "@colors/colors": "1.5.0" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, + "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": ">= 16" + "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 +5207,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 +5246,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 +5276,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,6 +5326,15 @@ } } }, + "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", @@ -2495,6 +5352,47 @@ "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": ">=0.4.0" + } + }, + "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": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2505,6 +5403,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 +5431,25 @@ "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/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true, + "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 +5464,39 @@ "node": ">=10.13.0" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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 +5504,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", @@ -2599,25 +5626,26 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -2636,7 +5664,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2808,6 +5836,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 +5861,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", @@ -2825,6 +5877,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2839,6 +5921,40 @@ "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/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2857,6 +5973,13 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2870,6 +5993,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2908,6 +6044,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 +6111,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", @@ -2944,7 +6128,55 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "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": { @@ -2973,6 +6205,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 +6225,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 +6252,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 +6311,52 @@ "hermes-estree": "0.25.1" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "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 +6394,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 +6420,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 +6442,29 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.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 +6472,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 +6570,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 +6634,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 +6664,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 +6945,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", @@ -3443,57 +6983,208 @@ "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lwk_web": { + "resolved": "vendor/lwk_web", + "link": true + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/marked-terminal": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", + "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <16" + } + }, + "node_modules/marked-terminal/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/marked-terminal/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8.6" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/lwk_web": { - "resolved": "../../../Blockstream/lwk/lwk_wasm/pkg_web", - "link": true + "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/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, + "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": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "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", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3503,13 +7194,41 @@ "node": "*" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3536,6 +7255,66 @@ "dev": true, "license": "MIT" }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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 +7322,45 @@ "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "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 +7379,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 +7454,22 @@ "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/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "dev": true, + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3606,11 +7483,34 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "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 +7556,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3663,6 +7564,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 +7630,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3699,6 +7647,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 +7684,57 @@ "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/publint": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.18.tgz", + "integrity": "sha512-JRJFeBTrfx4qLwEuGFPk+haJOJN97KnPuK01yj+4k/Wj5BgoOK5uNsivporiqBjk2JDaslg7qJOhGRnpltGeog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@publint/pack": "^0.1.4", + "package-manager-detector": "^1.6.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3735,11 +7745,45 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/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 +7810,53 @@ "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-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "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", @@ -3776,6 +7867,17 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -3821,6 +7923,121 @@ "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/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "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 +8054,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 +8090,52 @@ "dev": true, "license": "ISC" }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "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 +8146,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 +8169,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 +8225,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", @@ -3917,6 +8248,64 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz", @@ -3938,6 +8327,44 @@ "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/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "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 +8426,25 @@ "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.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 +8458,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 +8481,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" @@ -4039,37 +8492,173 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", - "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0" - }, + "node_modules/typescript-eslint": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": "20 || >=22" } }, - "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, - "license": "MIT" - }, "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 +8700,179 @@ "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/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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 +9984,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -5282,6 +10039,26 @@ } } }, + "node_modules/wallet-abi-sdk-alpha": { + "resolved": "vendor/wallet-abi-sdk-alpha", + "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 +10075,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 +10108,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 +10157,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 +10261,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 +10280,82 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zshy": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/zshy/-/zshy-0.7.0.tgz", + "integrity": "sha512-nBQv7JaVC+lUMK9+uD9LgXNHPsz3uo5gdk33ojCndY8hT4EkrGovwjacPsvB7BlTuV++i9bPQFFzWYrQyTH3DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "arg": "^5.0.2", + "fast-glob": "^3.3.2", + "table": "^6.9.0" + }, + "bin": { + "zshy": "dist/index.cjs" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/colinhacks" + }, + "peerDependencies": { + "typescript": ">5.5.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 + } + } + }, + "vendor/lwk_web": { + "name": "lwk_wasm", + "version": "0.15.0", + "license": "MIT OR BSD-2-Clause" + }, + "vendor/wallet-abi-sdk-alpha": { + "version": "0.2.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" + } } } } diff --git a/web/package.json b/web/package.json index 7eb4c7e..9c1c090 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:./vendor/lwk_web", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "wallet-abi-sdk-alpha": "file:./vendor/wallet-abi-sdk-alpha" }, "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/public/runtime-config.js b/web/public/runtime-config.js new file mode 100644 index 0000000..8c72996 --- /dev/null +++ b/web/public/runtime-config.js @@ -0,0 +1 @@ +window.__SIMPLICITY_LENDING_CONFIG__ = window.__SIMPLICITY_LENDING_CONFIG__ ?? {} diff --git a/web/src/App.tsx b/web/src/App.tsx index 897a245..ebad19c 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,223 +1,189 @@ -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, reconnect, 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..09311da 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -7,8 +7,14 @@ import type { ParticipantDto, ParticipantType, } from '../types/offers' +import { getApiBaseUrl } from '../config/runtimeConfig' +import { buildConfiguredUrl } from './url' -const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:8000' +const API_BASE = getApiBaseUrl() + +function buildApiUrl(path: string): string { + return buildConfiguredUrl(API_BASE, path) +} /** Parse u64 from JSON (number or string) to bigint; JSON has no native BigInt */ function toBigInt(v: unknown): bigint { @@ -38,7 +44,7 @@ export async function fetchOffers(params?: { limit?: number offset?: number }): Promise { - const url = new URL(`${API_BASE}/offers`) + const url = new URL(buildApiUrl('/offers')) if (params?.status) url.searchParams.set('status', params.status) if (params?.limit != null) url.searchParams.set('limit', String(params.limit)) if (params?.offset != null) url.searchParams.set('offset', String(params.offset)) @@ -68,7 +74,7 @@ async function throwIfNotOk(res: Response): Promise { } export async function fetchOfferIdsByScript(scriptPubkeyHex: string): Promise { - const url = new URL(`${API_BASE}/offers/by-script`) + const url = new URL(buildApiUrl('/offers/by-script')) url.searchParams.set('script_pubkey', scriptPubkeyHex) const res = await fetch(url.toString()) await throwIfNotOk(res) @@ -77,10 +83,22 @@ 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/, '') - const url = new URL(`${API_BASE}/offers/by-borrower-pubkey`) + const url = new URL(buildApiUrl('/offers/by-borrower-pubkey')) url.searchParams.set('borrower_pubkey', hex) const res = await fetch(url.toString()) await throwIfNotOk(res) @@ -115,11 +133,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( @@ -139,7 +174,7 @@ export function filterOffersByParticipantRole( } export async function fetchOfferDetailsBatch(ids: string[]): Promise { - const res = await fetch(`${API_BASE}/offers/batch`, { + const res = await fetch(buildApiUrl('/offers/batch'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids }), @@ -152,7 +187,7 @@ export async function fetchOfferDetailsBatch(ids: string[]): Promise { - const res = await fetch(`${API_BASE}/offers/batch`, { + const res = await fetch(buildApiUrl('/offers/batch'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids }), @@ -176,7 +211,7 @@ function normalizeOfferUtxo(raw: Record): OfferUtxo { /** Fetch UTXO history for an offer (GET /offers/:id/utxos). Used e.g. to get Lending UTXO for liquidation. */ export async function fetchOfferUtxos(offerId: string): Promise { - const res = await fetch(`${API_BASE}/offers/${encodeURIComponent(offerId)}/utxos`) + const res = await fetch(buildApiUrl(`/offers/${encodeURIComponent(offerId)}/utxos`)) await throwIfNotOk(res) const data: unknown[] = await res.json() return data.map((row) => normalizeOfferUtxo(row as Record)) @@ -200,7 +235,9 @@ function normalizeOfferParticipant(raw: Record): OfferParticipa /** Fetch participant movement history (GET /offers/:id/participants/history). Used to get current Lender/Borrower NFT (txid, vout) from indexer. */ export async function fetchOfferParticipantsHistory(offerId: string): Promise { - const res = await fetch(`${API_BASE}/offers/${encodeURIComponent(offerId)}/participants/history`) + const res = await fetch( + buildApiUrl(`/offers/${encodeURIComponent(offerId)}/participants/history`) + ) await throwIfNotOk(res) const data: unknown[] = await res.json() return data.map((row) => normalizeOfferParticipant(row as Record)) diff --git a/web/src/api/esplora.test.ts b/web/src/api/esplora.test.ts new file mode 100644 index 0000000..6479eec --- /dev/null +++ b/web/src/api/esplora.test.ts @@ -0,0 +1,62 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { DEFAULT_FEE_RATE_SAT_KVB, EsploraClient, resolveWalletFeeRateSatKvb } from './esplora' + +describe('Esplora fee estimates', () => { + const fetchMock = vi.fn() + + beforeEach(() => { + vi.stubGlobal('fetch', fetchMock) + }) + + afterEach(() => { + fetchMock.mockReset() + vi.unstubAllGlobals() + }) + + it('uses the exact target when Esplora returns it', async () => { + fetchMock.mockResolvedValueOnce( + new Response(JSON.stringify({ 1: 0.25, 6: 0.1 }), { status: 200 }) + ) + + const client = new EsploraClient('https://esplora.example') + + await expect(client.getFeeRateSatKvb(1)).resolves.toBe(250) + expect(fetchMock).toHaveBeenCalledWith('https://esplora.example/fee-estimates', { + signal: expect.any(AbortSignal), + }) + }) + + it('falls back to the next higher target when the exact one is missing', async () => { + fetchMock.mockResolvedValueOnce( + new Response(JSON.stringify({ 2: 0.5, 144: 0.1 }), { status: 200 }) + ) + + const client = new EsploraClient('https://esplora.example') + + await expect(client.getFeeRateSatKvb(1)).resolves.toBe(500) + }) + + it('uses any available numeric target when no higher target exists', async () => { + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ 144: 0.2 }), { status: 200 })) + + const client = new EsploraClient('https://esplora.example') + + await expect(client.getFeeRateSatKvb(1008)).resolves.toBe(200) + }) + + it('returns the hardcoded fallback when fee estimates are unusable', async () => { + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ foo: 'bar' }), { status: 200 })) + + const client = new EsploraClient('https://esplora.example') + + await expect(resolveWalletFeeRateSatKvb(client)).resolves.toBe(DEFAULT_FEE_RATE_SAT_KVB) + }) + + it('returns the hardcoded fallback when the fee request fails', async () => { + fetchMock.mockRejectedValueOnce(new Error('network down')) + + const client = new EsploraClient('https://esplora.example') + + await expect(resolveWalletFeeRateSatKvb(client)).resolves.toBe(DEFAULT_FEE_RATE_SAT_KVB) + }) +}) diff --git a/web/src/api/esplora.ts b/web/src/api/esplora.ts index dc8ee7b..c25b95e 100644 --- a/web/src/api/esplora.ts +++ b/web/src/api/esplora.ts @@ -1,30 +1,72 @@ /** * 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). */ +import { getEsploraApiBaseUrl, getEsploraExplorerBaseUrl } from '../config/runtimeConfig' + /** Default request timeout in milliseconds. */ export const DEFAULT_TIMEOUT_MS = 30_000 +export const DEFAULT_ESPLORA_FEE_TARGET_BLOCKS = 1 +export const DEFAULT_FEE_RATE_SAT_KVB = 100 -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 { + return normalizeBaseUrl(getEsploraApiBaseUrl()) } function getExplorerBaseUrl(apiBaseUrl: string): string { - const env = import.meta.env.VITE_ESPLORA_EXPLORER_URL - if (typeof env === 'string' && env.trim()) { - return env.trim().replace(/\/+$/, '') + const configured = getEsploraExplorerBaseUrl() + if (typeof configured === 'string' && configured.trim()) { + return configured.trim().replace(/\/+$/, '') } return apiBaseUrl } +function isValidFeeEstimateEntry(target: number, rate: number): boolean { + return Number.isInteger(target) && target > 0 && Number.isFinite(rate) && rate > 0 +} + +function parseFeeEstimateEntries( + estimates: Record +): Array { + return Object.entries(estimates) + .map(([target, rate]) => [Number(target), rate] as const) + .filter(([target, rate]) => isValidFeeEstimateEntry(target, rate)) + .sort(([leftTarget], [rightTarget]) => leftTarget - rightTarget) +} + +export function selectFeeRateSatVb( + estimates: Record, + targetBlocks: number +): number { + const entries = parseFeeEstimateEntries(estimates) + if (entries.length === 0) { + throw new EsploraApiError('No fee estimates available') + } + + const exactEntry = entries.find(([target]) => target === targetBlocks) + if (exactEntry) { + return exactEntry[1] + } + + const higherTargetEntry = entries.find(([target]) => target > targetBlocks) + if (higherTargetEntry) { + return higherTargetEntry[1] + } + + return entries[0][1] +} + export class EsploraApiError extends Error { readonly status: number | undefined readonly body: string | undefined @@ -40,18 +82,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 +124,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 +145,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 +169,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 +177,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). */ @@ -146,6 +199,39 @@ export class EsploraClient { return height } + /** GET /fee-estimates — confirmation target to fee rate map in sat/vB. */ + async getFeeEstimates(): Promise> { + const body = await this.get('/fee-estimates') + try { + const raw = JSON.parse(body) as unknown + if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) { + throw new EsploraApiError('Expected fee estimate object') + } + + return Object.fromEntries( + Object.entries(raw).filter( + ([target, rate]) => + Number.isInteger(Number(target)) && + Number(target) > 0 && + typeof rate === 'number' && + Number.isFinite(rate) && + rate > 0 + ) + ) + } catch (e) { + if (e instanceof EsploraApiError) throw e + throw new EsploraApiError( + `Failed to parse fee estimates: ${e instanceof Error ? e.message : String(e)}` + ) + } + } + + /** Resolve a fee rate for the target in sats/kvB. */ + async getFeeRateSatKvb(targetBlocks: number): Promise { + const feeRateSatVb = selectFeeRateSatVb(await this.getFeeEstimates(), targetBlocks) + return feeRateSatVb * 1000 + } + /** Block hash at a given height. */ async getBlockHashAtHeight(blockHeight: number): Promise { const body = await this.get(`/block-height/${blockHeight}`) @@ -340,8 +426,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 } @@ -362,6 +453,18 @@ export interface EsploraOutspend { [key: string]: unknown } +export async function resolveWalletFeeRateSatKvb( + esplora: Pick, + targetBlocks: number = DEFAULT_ESPLORA_FEE_TARGET_BLOCKS, + fallbackFeeRateSatKvb: number = DEFAULT_FEE_RATE_SAT_KVB +): Promise { + try { + return await esplora.getFeeRateSatKvb(targetBlocks) + } catch { + return fallbackFeeRateSatKvb + } +} + /** * Hash script_pubkey (hex) to 32-byte script hash (SHA256). * Matches Rust hash_script; use this for PreLockArguments script hashes. diff --git a/web/src/api/url.test.ts b/web/src/api/url.test.ts new file mode 100644 index 0000000..006fc66 --- /dev/null +++ b/web/src/api/url.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' + +import { buildConfiguredUrl } from './url' + +describe('buildConfiguredUrl', () => { + it('keeps absolute API bases absolute', () => { + expect(buildConfiguredUrl('https://api.example.com', '/offers')).toBe( + 'https://api.example.com/offers' + ) + }) + + it('resolves root-relative API bases against the browser origin', () => { + expect(buildConfiguredUrl('/api', '/offers', 'https://app.example.com')).toBe( + 'https://app.example.com/api/offers' + ) + }) + + it('resolves plain relative API bases against the browser origin', () => { + expect(buildConfiguredUrl('api', 'offers/by-borrower-pubkey', 'https://app.example.com')).toBe( + 'https://app.example.com/api/offers/by-borrower-pubkey' + ) + }) +}) diff --git a/web/src/api/url.ts b/web/src/api/url.ts new file mode 100644 index 0000000..a7f2c3f --- /dev/null +++ b/web/src/api/url.ts @@ -0,0 +1,26 @@ +function hasUrlScheme(value: string): boolean { + return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value) +} + +function joinUrlParts(baseUrl: string, path: string): string { + const normalizedBase = baseUrl.replace(/\/+$/, '') + const normalizedPath = path.replace(/^\/+/, '') + return `${normalizedBase}/${normalizedPath}` +} + +export function buildConfiguredUrl( + baseUrl: string, + path: string, + origin?: string +): string { + const joined = joinUrlParts(baseUrl, path) + if (hasUrlScheme(joined)) { + return joined + } + + const fallbackOrigin = + origin ?? + (typeof window !== 'undefined' ? window.location.origin : 'http://localhost') + + return new URL(joined, fallbackOrigin).toString() +} 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