From b6475b18be06d2ffe8f5faa436481b166eb5f0f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20K=C3=B6hler?= Date: Wed, 16 Sep 2026 11:22:45 +0200 Subject: [PATCH 1/7] Run the proof verifier in the browser instead of on a server Electrum needs a TCP socket a page cannot open, so the UTXO lookups moved to Esplora over HTTP. The server is configurable and defaults to Blockstream's public instances. bitcoinconsensus is Bitcoin Core's script interpreter and therefore C++, so building it for wasm needs a C++ standard library for that target. build.sh borrows clang and libc++ from the wasi-sdk, but does not link wasi-libc: that would put a second allocator next to Rust's and add a WASI import surface the browser would have to fill in. src/csupport.rs backs libc++ with Rust's allocator instead, so the module imports nothing but its own glue. Also fixes the confirmation depth, which was off by one. The block a UTXO is mined in counts as its first confirmation, so asking for three quietly required four. --- .cargo/wasm-patch.toml | 4 + .dockerignore | 5 - .github/workflows/cont_integration.yml | 30 +- .github/workflows/pages.yml | 69 + .github/workflows/release.yml | 49 +- .gitignore | 14 + CHANGELOG.md | 39 + Cargo.lock | 1767 ++--------------- Cargo.toml | 29 +- Dockerfile | 23 - Makefile | 40 +- README.md | 74 +- build.sh | 86 + examples/dump_outpoints.rs | 28 + patches/libc/Cargo.toml | 22 + patches/libc/src/lib.rs | 3 + res/index.html | 79 - src/csupport.rs | 179 ++ src/esplora.rs | 238 +++ src/lib.rs | 115 ++ src/main.rs | 184 -- src/proof.rs | 194 ++ tests/e2e.cjs | 162 ++ tests/fixtures/outpoints.txt | 18 + ...fd77205a144e92677e603db69ee76a2358fe86c2ec | 1 + ...fa9eb422e91a8b5edb3d16f12c6f93ad8ffc47e62b | 1 + ...10fe0c89464ebb47e0663e336c69817d48e9586a17 | 1 + ...6cbbb0454523127098e8b0aa41dda005a9290e05d4 | 1 + ...dfdf8d444b5371307d25216827fb1eff337c8a67d1 | 1 + ...f2ddba80179e8ff7f3f2fdfbbf85b505a4980aec72 | 1 + ...4c0d108d8a2af1321c2bc6295e7d4e4b00adbea08c | 1 + ...0c50fa7a0561c4b298f93ab38d06a609a41a8d22c3 | 1 + ...bedf37f684b39a96d3787bc4ce376143d65e596415 | 1 + ...76f54f67049875ce639f90a5c1d42dae3d8262c4b7 | 1 + ...c25a45b92dfe0fa086178815ba08d84791a6b5aa2b | 1 + ...8d92720f152c162eb0353988b63e1412c4852399f2 | 1 + web/app.js | 157 ++ web/example/proof.json | 9 + web/example/proof.psbt.base64 | 1 + web/index.html | 152 ++ 40 files changed, 1815 insertions(+), 1967 deletions(-) create mode 100644 .cargo/wasm-patch.toml delete mode 100644 .dockerignore create mode 100644 .github/workflows/pages.yml delete mode 100644 Dockerfile create mode 100755 build.sh create mode 100644 examples/dump_outpoints.rs create mode 100644 patches/libc/Cargo.toml create mode 100644 patches/libc/src/lib.rs delete mode 100644 res/index.html create mode 100644 src/csupport.rs create mode 100644 src/esplora.rs create mode 100644 src/lib.rs delete mode 100644 src/main.rs create mode 100644 src/proof.rs create mode 100644 tests/e2e.cjs create mode 100644 tests/fixtures/outpoints.txt create mode 100644 tests/fixtures/transactions/008883eada2a57f80a18b4fd77205a144e92677e603db69ee76a2358fe86c2ec create mode 100644 tests/fixtures/transactions/247b272f041fd14357c668fa9eb422e91a8b5edb3d16f12c6f93ad8ffc47e62b create mode 100644 tests/fixtures/transactions/2afc04742e639fb679bfa210fe0c89464ebb47e0663e336c69817d48e9586a17 create mode 100644 tests/fixtures/transactions/6bcd3c35a063f9a45075916cbbb0454523127098e8b0aa41dda005a9290e05d4 create mode 100644 tests/fixtures/transactions/6e6697fcfc78641e960803dfdf8d444b5371307d25216827fb1eff337c8a67d1 create mode 100644 tests/fixtures/transactions/7d252165dbd5b147f47b0cf2ddba80179e8ff7f3f2fdfbbf85b505a4980aec72 create mode 100644 tests/fixtures/transactions/953a78a6773d83e0539d5e4c0d108d8a2af1321c2bc6295e7d4e4b00adbea08c create mode 100644 tests/fixtures/transactions/9d5ce4c39fe62f7567737d0c50fa7a0561c4b298f93ab38d06a609a41a8d22c3 create mode 100644 tests/fixtures/transactions/c510fd107826af20ef7981bedf37f684b39a96d3787bc4ce376143d65e596415 create mode 100644 tests/fixtures/transactions/cc7173bed4defad6a6304d76f54f67049875ce639f90a5c1d42dae3d8262c4b7 create mode 100644 tests/fixtures/transactions/d979c1072803a92cff5e36c25a45b92dfe0fa086178815ba08d84791a6b5aa2b create mode 100644 tests/fixtures/transactions/e9170ceec57cefd0a9eba18d92720f152c162eb0353988b63e1412c4852399f2 create mode 100644 web/app.js create mode 100644 web/example/proof.json create mode 100644 web/example/proof.psbt.base64 create mode 100644 web/index.html diff --git a/.cargo/wasm-patch.toml b/.cargo/wasm-patch.toml new file mode 100644 index 0000000..d29b2f1 --- /dev/null +++ b/.cargo/wasm-patch.toml @@ -0,0 +1,4 @@ +# Passed to cargo as `--config .cargo/wasm-patch.toml` by build.sh, so the libc +# stand-in applies to the wasm build only and native `cargo test` is unaffected. +[patch.crates-io] +libc = { path = "patches/libc" } diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 7a17791..0000000 --- a/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -target/ -.git/ -.github/ -Dockerfile -Makefile diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 1e69c3c..16da3e6 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -27,38 +27,40 @@ jobs: - name: Generate cache key run: echo "${{ matrix.rust.version }}" | tee .cache_key - name: cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target + .tools key: ${{ runner.os }}-cargo-${{ hashFiles('.cache_key') }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} - name: Set default toolchain run: rustup default ${{ matrix.rust.version }} - name: Set profile run: rustup set profile minimal + - name: Add the wasm target + run: rustup target add wasm32-unknown-unknown - name: Add clippy if: ${{ matrix.rust.clippy }} run: rustup component add clippy - name: Update toolchain run: rustup update - - name: Build - run: cargo build + - name: Test the verification logic + run: cargo test - name: Clippy if: ${{ matrix.rust.clippy }} run: cargo clippy --all-targets -- -D warnings - - name: Test - run: cargo test - - build-docker: - name: build docker - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build the Docker image - run: docker build --file Dockerfile --tag bdk-reserves-web . + - name: Build the static site + run: make build + - name: Test the built module end to end + run: make test-e2e + - name: Upload the site + if: ${{ matrix.rust.clippy }} + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist fmt: name: Rust fmt diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..d4ff591 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,69 @@ +name: Pages + +# Publishes the static site to https://aminabank.github.io/bdk-reserves-web/ +# +# Needs "Build and deployment > Source" set to "GitHub Actions" once, under the +# repository's Settings > Pages. Without that the deploy step fails. + +on: + push: + branches: + - 'master' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Let a running deploy finish rather than cancelling it half way, but queue at +# most one behind it. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + name: Build and deploy + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set default toolchain + run: rustup default 1.83.0 + - name: Add the wasm target + run: rustup target add wasm32-unknown-unknown + + # The wasi-sdk is a ~130MB download, so keep it between runs. + - name: Cache the build tools + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + .tools + key: ${{ runner.os }}-pages-${{ hashFiles('**/Cargo.lock', 'build.sh') }} + + # Build and test together, so a broken proof verifier never reaches the + # published site. make test-e2e builds dist/ as a side effect. + - name: Build and test + run: make test + + # Pages runs Jekyll otherwise, which skips files it does not expect. + - name: Disable Jekyll + run: touch dist/.nojekyll + + - name: Configure Pages + uses: actions/configure-pages@v5 + - name: Upload the site + uses: actions/upload-pages-artifact@v3 + with: + path: dist + - name: Deploy + id: deploy + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c61f9b..4a52dda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,41 +20,22 @@ jobs: # (required) GitHub token for creating GitHub Releases. token: ${{ secrets.GITHUB_TOKEN }} - upload-assets: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: taiki-e/upload-rust-binary-action@v1 - with: - # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. - # Note that glob pattern is not supported yet. - bin: bdk-reserves-web - # (required) GitHub token for uploading assets to GitHub Releases. - token: ${{ secrets.GITHUB_TOKEN }} - - heroku-deploy: + # The app is a static site now, so the release asset is the site itself + # rather than a server binary. + upload-site: runs-on: ubuntu-latest + needs: create-release steps: - uses: actions/checkout@v4 - name: Set default toolchain - run: rustup default 1.72.0 - - name: Build - run: cargo build --release - - name: make dir - run: mkdir app - - name: copy - run: cp ./target/release/bdk-reserves-web ./app/ - - uses: akhileshns/heroku-deploy@v3.12.12 - with: - # key from repository secrets - heroku_api_key: ${{secrets.HEROKU_API_KEY}} - # name of the Heroku app - heroku_app_name: "bdk-reserves-web" - # app directory - appdir: "./app" - # start command - procfile: "web: ./bdk-reserves-web" - # email from which the app is uploaded - heroku_email: "richi+heroku@ulrichard.ch" - # buildpack is like environment used to run the app - buildpack: "https://github.com/ph3nx/heroku-binary-buildpack.git" \ No newline at end of file + run: rustup default 1.83.0 + - name: Add the wasm target + run: rustup target add wasm32-unknown-unknown + - name: Build the static site + run: make build + - name: Package it + run: tar czf bdk-reserves-web-${{ github.ref_name }}.tar.gz -C dist . + - name: Attach it to the release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload ${{ github.ref_name }} bdk-reserves-web-*.tar.gz diff --git a/.gitignore b/.gitignore index ea8c4bf..8ca33ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,15 @@ +# Build output /target +/dist + +# Toolchains build.sh fetches (wasi-sdk, wasm-bindgen) +/.tools + +# Written by the CI workflow when its steps are run locally +/.cache_key + +# Editor and OS noise +.DS_Store +*.swp +/.idea/ +/.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4354eca..6e04258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Summary + +The app now runs entirely in the browser. There is no backend left. + +### Changed + +- Verification is compiled to WebAssembly and runs in the page. The PSBT is no + longer uploaded to a server. +- UTXO lookups moved from Electrum to Esplora over HTTP, because a browser + cannot open the TCP connection Electrum needs. The server is configurable. +- The network is worked out by parsing the addresses instead of guessing from + the first character, so testnet bech32 and signet addresses now work. +- Amounts come from the transactions that created each UTXO, and each one is + checked against the txid requested, so the Esplora server cannot inflate the + reported total. +- Confirmation depth is configurable in the UI, still defaulting to 3. +- The page has a "Load example" button that fills in a real testnet proof, + along with a note explaining that its UTXOs have since been spent. +- Verification failures are explained in plain language rather than only as the + enum variant `bdk-reserves` returns. The raw message is still shown. +- The build output is a folder of static files, so `make serve` is enough to + run it locally and any static host can serve it. The Dockerfile and the + Heroku deployment are gone. + +### Fixed + +- Confirmation depth was off by one. The block a UTXO is mined in counts as its + first confirmation, but the old rule treated it as the zeroth, so asking for + 3 confirmations quietly required 4. + +### Added + +- A GitHub Pages workflow that publishes the site on every push to `master`, + gated on the test suite passing. + +### Removed + +- The actix-web HTTP service and its `/proof` endpoint. + ## [v0.1.10] ### Summary diff --git a/Cargo.lock b/Cargo.lock index 4d78fa9..caf7e7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,204 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash", - "base64 0.22.1", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "ahash" version = "0.8.11" @@ -207,36 +9,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", "once_cell", "version_check", "zerocopy", ] -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "arrayvec" version = "0.7.6" @@ -249,46 +26,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "aws-lc-rs" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca" -dependencies = [ - "aws-lc-sys", - "paste", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", - "paste", -] - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - [[package]] name = "base58ck" version = "0.1.0" @@ -305,12 +42,6 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bdk-reserves" version = "1.0.0" @@ -326,13 +57,19 @@ dependencies = [ name = "bdk-reserves-web" version = "0.2.0" dependencies = [ - "actix-web", "bdk-reserves", - "bdk_electrum", "bdk_wallet", + "console_error_panic_hook", + "futures-util", + "getrandom", + "js-sys", "log", "serde", + "serde-wasm-bindgen", "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -354,20 +91,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b545aea1efc090e4f71f1dd5468090d9f54c3de48002064c04895ef811fbe0b2" dependencies = [ "bitcoin", - "hashbrown 0.14.5", + "hashbrown", "serde", ] -[[package]] -name = "bdk_electrum" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b272d5a3228799f7c917255fe26e788f6c29dd4a084a342d274a44352bbc0915" -dependencies = [ - "bdk_core", - "electrum-client", -] - [[package]] name = "bdk_wallet" version = "1.0.0" @@ -388,29 +115,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", - "which", -] - [[package]] name = "bitcoin" version = "0.32.5" @@ -418,7 +122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", - "base64 0.21.7", + "base64", "bech32", "bitcoin-internals", "bitcoin-io", @@ -477,40 +181,10 @@ dependencies = [ ] [[package]] -name = "bitflags" -version = "2.8.0" +name = "bumpalo" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotli" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -518,41 +192,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "bytes" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" - -[[package]] -name = "bytestring" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" -dependencies = [ - "bytes", -] - [[package]] name = "cc" version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ - "jobserver", - "libc", "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -560,741 +208,142 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "clang-sys" -version = "1.8.1" +name = "console_error_panic_hook" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "glob", - "libc", - "libloading", + "cfg-if", + "wasm-bindgen", ] [[package]] -name = "cmake" -version = "0.1.52" +name = "futures-core" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" -dependencies = [ - "cc", -] +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] -name = "convert_case" -version = "0.4.0" +name = "futures-task" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] -name = "cookie" -version = "0.16.2" +name = "futures-util" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "percent-encoding", - "time", - "version_check", + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", ] [[package]] -name = "cpufeatures" -version = "0.2.16" +name = "getrandom" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ + "cfg-if", + "js-sys", "libc", + "wasi", + "wasm-bindgen", ] [[package]] -name = "crc32fast" -version = "1.4.2" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "cfg-if", + "ahash", + "serde", ] [[package]] -name = "crypto-common" -version = "0.1.6" +name = "hex-conservative" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" dependencies = [ - "generic-array", - "typenum", + "arrayvec", ] [[package]] -name = "deranged" -version = "0.3.11" +name = "hex_lit" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" [[package]] -name = "derive_more" -version = "0.99.18" +name = "itoa" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] -name = "digest" -version = "0.10.7" +name = "js-sys" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ - "block-buffer", - "crypto-common", + "cfg-if", + "futures-util", + "wasm-bindgen", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "libc" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] -name = "dunce" -version = "1.0.5" +name = "log" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] -name = "either" -version = "1.13.0" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] -name = "electrum-client" -version = "0.22.0" +name = "miniscript" +version = "12.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d627e4feaf3009c10c8a0eb06d6ceb4ce1ff861849157fb35e8155d9706babb6" +checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" dependencies = [ + "bech32", "bitcoin", - "byteorder", - "libc", - "log", - "rustls", "serde", - "serde_json", - "webpki-roots", - "winapi", ] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "once_cell" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] -name = "equivalent" -version = "1.0.1" +name = "pin-project-lite" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "flate2" -version = "1.0.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "serde", -] - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "hex-conservative" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hex_lit" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "httparse" -version = "1.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - -[[package]] -name = "indexmap" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" -dependencies = [ - "equivalent", - "hashbrown 0.15.2", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" - -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "libloading" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" -dependencies = [ - "cfg-if", - "windows-targets", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "litemap" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" -dependencies = [ - "bech32", - "bitcoin", - "serde", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.52.0", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1302,191 +351,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +] [[package]] -name = "rustc-hash" -version = "1.1.0" +name = "proc-macro2" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] [[package]] -name = "rustc_version" -version = "0.4.1" +name = "quote" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ - "semver", + "proc-macro2", ] [[package]] -name = "rustix" -version = "0.38.44" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "bitflags", - "errno", "libc", - "linux-raw-sys", - "windows-sys 0.59.0", + "rand_chacha", + "rand_core", ] [[package]] -name = "rustls" -version = "0.23.21" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "ppv-lite86", + "rand_core", ] [[package]] -name = "rustls-pki-types" -version = "1.10.1" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] -name = "rustls-webpki" -version = "0.102.8" +name = "rustversion" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -1494,12 +413,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "secp256k1" version = "0.29.1" @@ -1521,12 +434,6 @@ dependencies = [ "cc", ] -[[package]] -name = "semver" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" - [[package]] name = "serde" version = "1.0.217" @@ -1536,6 +443,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.217" @@ -1559,44 +477,12 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "slab" version = "0.4.9" @@ -1606,40 +492,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.96" @@ -1651,149 +503,12 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-util" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - [[package]] name = "unicode-ident" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "version_check" version = "0.9.5" @@ -1807,161 +522,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "webpki-roots" -version = "0.25.4" +name = "wasm-bindgen" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ - "either", - "home", + "cfg-if", "once_cell", - "rustix", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] -name = "winapi" -version = "0.3.9" +name = "wasm-bindgen-futures" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "wasm-bindgen-macro" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ - "windows-targets", + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "wasm-bindgen-macro-support" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "yoke" -version = "0.7.5" +name = "wasm-bindgen-shared" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", + "unicode-ident", ] [[package]] -name = "yoke-derive" -version = "0.7.5" +name = "web-sys" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -1984,80 +606,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zerofrom" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zstd" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index d071e8b..4d83b21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,17 +2,38 @@ name = "bdk-reserves-web" version = "0.2.0" edition = "2021" +description = "Browser-only verifier for BIP-127 proof of reserves PSBTs" +license = "MIT OR Apache-2.0" + +[lib] +crate-type = ["cdylib", "rlib"] [dependencies] -actix-web = "4" +bdk_wallet = { version = "1.0.0", default-features = false, features = ["std"] } +bdk-reserves = { git = "https://github.com/AminaBank/bdk-reserves", branch = "feature/v1" } serde = { version = "1", features = ["derive"] } serde_json = "1" -bdk_wallet = "1.0.0" -bdk-reserves = { git = "https://github.com/AminaBank/bdk-reserves", branch = "feature/v1" } -bdk_electrum = "0.20" log = "0.4" +# wasm-bindgen is pinned because the CLI that post-processes the module has to +# match the crate exactly. bump both together, see build.sh. +wasm-bindgen = "=0.2.127" +wasm-bindgen-futures = "0.4" +serde-wasm-bindgen = "0.6" +js-sys = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } +console_error_panic_hook = "0.1" + +# secp256k1 needs a randomness source for context re-randomisation, which on +# wasm has to come from the browser. +getrandom = { version = "0.2", features = ["js"] } + +[dependencies.web-sys] +version = "0.3" +features = ["Request", "RequestInit", "Response"] + [dev-dependencies] +bdk_wallet = { version = "1.0.0", features = ["std"] } [profile.release] opt-level = 'z' # Optimize for size diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2bc0d20..0000000 --- a/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM rust:1.83-alpine3.20 as builder -RUN apk add --no-cache build-base -#USER bin -WORKDIR /app -COPY . . -#RUN ls -lha -RUN cargo test -RUN cargo build --release -RUN install -D target/release/bdk-reserves-web dist/bin/bdk-reserves-web -RUN ldd dist/bin/bdk-reserves-web | tr -s [:blank:] '\n' | grep ^/ | xargs -I % install -D % dist/% -RUN ln -s ld-musl-x86_64.so.1 dist/lib/libc.musl-x86_64.so.1 - -RUN rustup component add clippy-preview \ - && rustup component add rustfmt -RUN cargo install cargo-audit -RUN cargo fmt -- --check -RUN cargo clippy -RUN cargo audit - -FROM scratch -COPY --from=builder /app/dist / -USER 65534 -ENTRYPOINT ["/bin/bdk-reserves-web"] \ No newline at end of file diff --git a/Makefile b/Makefile index b9be5e5..0b79058 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,37 @@ -TAG := bdk-reserves-web +.PHONY: build serve test test-unit test-e2e fmt clippy clean -run: builder - docker run --rm --tty --env PORT=8888 --publish 8888:8888 ${TAG} +PORT ?= 8087 -builder: - docker build --tag ${TAG} . +# Build the static site into dist/. +build: + ./build.sh + +# Serve dist/ locally. Any static file server will do, since dist/ is just +# files. The wasm module needs http rather than file://. +serve: build + @echo "Serving http://localhost:$(PORT)" + @cd dist && python3 -m http.server $(PORT) + +test: test-unit test-e2e + +# Verification logic, on the host, no network. +test-unit: + cargo test + +# The built wasm module driven through its real JS API against a mock Esplora. +test-e2e: build + @rm -rf target/e2e + @$$(command -v wasm-bindgen || echo .tools/bin/wasm-bindgen) \ + --target nodejs --no-typescript --out-dir target/e2e \ + target/wasm32-unknown-unknown/release/bdk_reserves_web.wasm + node tests/e2e.cjs + +fmt: + cargo fmt --all + +clippy: + cargo clippy --all-targets -- -D warnings + +clean: + cargo clean + rm -rf dist .tools diff --git a/README.md b/README.md index b35092f..00a44b7 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,12 @@

- Proof of reserves for Bitcoin dev kit - web app + Proof of reserves for Bitcoin dev kit - browser app

MIT or Apache-2.0 Licensed CI Status - Rustc Version 1.68.2+ - Heroku

@@ -27,19 +25,79 @@ The `bdk` library aims to be the core building block for Bitcoin wallets of any kind. The `bdk-reserves` library provides an implementation of `proof-of-reserves` for bdk. -The `bdk-reserves-web` is a simple web app to validate the proofs. +The `bdk-reserves-web` is a web app to validate the proofs. * It validates proofs in the form of PSBT's. * The implementation was inspired by BIP-0127 and BIP-0322. -## Heroku -The web app is currently deployed to heroku, and can be reached here: -bdk-reserves-web +Verification runs entirely in the browser, compiled to WebAssembly. There is no +backend: `dist/` is a folder of static files that any web server, CDN or +GitHub Pages site can host. The PSBT you paste in is never uploaded anywhere. + +The only network traffic is UTXO lookups against an [Esplora](https://github.com/Blockstream/esplora) +server, which default to Blockstream's public instances. You can point the app +at your own Esplora instead, and should if you do not want to tell a third party +which addresses you are checking. + +## Verification + +The proof itself is checked by `bdk-reserves`, unchanged from the server version. +Script signatures are validated by `bitcoinconsensus`, which is Bitcoin Core's +own script interpreter, so a signature accepted here is one the network would +accept. Amounts are taken from the transactions that created each UTXO, and each +transaction is checked against the txid that was requested, so an Esplora server +cannot inflate the reported total by misreporting a value. + +What you still trust the Esplora server for is which outputs are *unspent*. A +server that hides a UTXO makes a valid proof fail, which is the safe direction. + +## Building + +Needs a Rust toolchain, `curl` and `node` (for the end to end test). Everything +else is fetched into `.tools/` on first build. + +```shell +make build # produces dist/ +make serve # builds, then serves dist/ on http://localhost:8087 +make test # verification logic on the host, plus the built module end to end +``` + +`make serve` runs `python3 -m http.server` over `dist/`, which is all a dev +build needs. To publish it elsewhere, copy `dist/` anywhere that serves static +files: S3, a CDN, or any web server. The `.wasm` file should be served as +`application/wasm`, which most of them do already. + +### Why a C++ toolchain gets downloaded + +`bitcoinconsensus` compiles Bitcoin Core's script interpreter, which is C++, so +building for wasm needs a C++ standard library for that target. `build.sh` +fetches the [wasi-sdk](https://github.com/WebAssembly/wasi-sdk) and uses its +clang and libc++. + +It deliberately does not link wasi-libc, because that would put a second +allocator in the module next to Rust's and add WASI imports the browser would +have to fill in. `src/csupport.rs` supplies the dozen or so C symbols libc++ +actually needs instead, with `malloc` and `free` forwarding to Rust's allocator. +The module that comes out imports nothing but its own JS glue. + +Set `WASI_SDK_PATH` to reuse an SDK you already have. + +## Deployment + +Every push to `master` publishes to +aminabank.github.io/bdk-reserves-web +via `.github/workflows/pages.yml`. The workflow runs the full test suite first, +so a broken verifier does not reach the published site. + +This needs **Settings > Pages > Build and deployment > Source** set to +**GitHub Actions**, once. Until that is done the deploy step fails. + +Pages serves the app from a subdirectory rather than a domain root, which is why +everything in `web/` refers to its assets relatively. ## Sponsorship The implementation of bdk-reserves-web was sponsored by SEBA Bank. - ## License Licensed under either of diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..6c5338b --- /dev/null +++ b/build.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# +# Build the wasm module and assemble the static site into dist/. +# +# bitcoinconsensus compiles Bitcoin Core's script interpreter, so this needs a +# C++ toolchain targeting wasm. The wasi-sdk provides one; we borrow its clang +# and its libc++ but deliberately do not link wasi-libc, because that would add +# a second allocator and a WASI import surface. src/csupport.rs supplies the +# handful of C symbols libc++ actually needs instead. + +set -euo pipefail + +cd "$(dirname "$0")" + +WASI_SDK_VERSION="${WASI_SDK_VERSION:-25.0}" +WASM_BINDGEN_VERSION="${WASM_BINDGEN_VERSION:-0.2.127}" # keep in step with Cargo.toml +TOOLS_DIR="${TOOLS_DIR:-$PWD/.tools}" +WASI_SDK_PATH="${WASI_SDK_PATH:-$TOOLS_DIR/wasi-sdk-${WASI_SDK_VERSION}-x86_64-linux}" + +LOCKFILE_BACKUP="$(mktemp)" + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } + +if [ ! -d "$WASI_SDK_PATH" ]; then + log "Fetching wasi-sdk ${WASI_SDK_VERSION}" + mkdir -p "$TOOLS_DIR" + major="${WASI_SDK_VERSION%%.*}" + curl -fsSL --retry 3 \ + "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${major}/wasi-sdk-${WASI_SDK_VERSION}-x86_64-linux.tar.gz" \ + | tar xz -C "$TOOLS_DIR" +fi + +WASM_BINDGEN="$(command -v wasm-bindgen || true)" +if [ -z "$WASM_BINDGEN" ] || ! "$WASM_BINDGEN" --version | grep -q "$WASM_BINDGEN_VERSION"; then + WASM_BINDGEN="$TOOLS_DIR/bin/wasm-bindgen" + if [ ! -x "$WASM_BINDGEN" ] || ! "$WASM_BINDGEN" --version | grep -q "$WASM_BINDGEN_VERSION"; then + log "Fetching wasm-bindgen ${WASM_BINDGEN_VERSION}" + mkdir -p "$TOOLS_DIR/bin" + curl -fsSL --retry 3 \ + "https://github.com/rustwasm/wasm-bindgen/releases/download/${WASM_BINDGEN_VERSION}/wasm-bindgen-${WASM_BINDGEN_VERSION}-x86_64-unknown-linux-musl.tar.gz" \ + | tar xz -C "$TOOLS_DIR/bin" --strip-components=1 \ + "wasm-bindgen-${WASM_BINDGEN_VERSION}-x86_64-unknown-linux-musl/wasm-bindgen" + fi +fi + +rustup target add wasm32-unknown-unknown >/dev/null 2>&1 || true + +SYSROOT="$WASI_SDK_PATH/share/wasi-sysroot" + +# The C++ sources build against the wasi-wasip1 sysroot; the Rust side stays on +# wasm32-unknown-unknown. Only libc++ and libc++abi get linked, never libc. +export CC_wasm32_unknown_unknown="$WASI_SDK_PATH/bin/clang" +export CXX_wasm32_unknown_unknown="$WASI_SDK_PATH/bin/clang++" +export AR_wasm32_unknown_unknown="$WASI_SDK_PATH/bin/llvm-ar" +export CFLAGS_wasm32_unknown_unknown="--target=wasm32-wasip1" +# The release profile sets panic=abort, which makes cc-rs pass -fno-exceptions. +# tinyformat does not compile that way, so switch exceptions back on; the throw +# path itself lands on the trapping stubs in src/csupport.rs. +export CXXFLAGS_wasm32_unknown_unknown="--target=wasm32-wasip1 -fexceptions" +export CXXSTDLIB_wasm32_unknown_unknown="c++" +export RUSTFLAGS="-L $SYSROOT/lib/wasm32-wasip1 -C link-arg=-lc++abi" + +# Patching libc turns it into a path dependency, which rewrites its entry in +# Cargo.lock. The committed lockfile describes the native build, so keep that +# the canonical one and put it back afterwards rather than letting the two +# builds fight over it. +if [ -f Cargo.lock ]; then + cp Cargo.lock "$LOCKFILE_BACKUP" + trap 'mv -f "$LOCKFILE_BACKUP" Cargo.lock 2>/dev/null || true' EXIT +fi + +log "Building the wasm module" +cargo build --release --target wasm32-unknown-unknown --config .cargo/wasm-patch.toml + +log "Generating JS bindings" +rm -rf dist +mkdir -p dist +"$WASM_BINDGEN" --target web --no-typescript --out-dir dist \ + target/wasm32-unknown-unknown/release/bdk_reserves_web.wasm + +log "Copying the static site" +cp web/index.html web/app.js dist/ +cp -r web/example dist/example +cp -r static dist/static + +log "Done. dist/ is a static site: $(du -sh dist | cut -f1)" diff --git a/examples/dump_outpoints.rs b/examples/dump_outpoints.rs new file mode 100644 index 0000000..202068a --- /dev/null +++ b/examples/dump_outpoints.rs @@ -0,0 +1,28 @@ +//! Prints the outpoints a proof PSBT claims, one ` ` per line. +//! +//! This is how `tests/fixtures/outpoints.txt` was generated, and how to +//! regenerate it if the proof fixture is ever replaced. The transactions +//! themselves come from an Esplora server: +//! +//! ```text +//! cargo run --example dump_outpoints > tests/fixtures/outpoints.txt +//! cut -d' ' -f1 tests/fixtures/outpoints.txt | sort -u | while read txid; do +//! curl -s "https://blockstream.info/testnet/api/tx/$txid/hex" \ +//! -o "tests/fixtures/transactions/$txid" +//! done +//! ``` + +use bdk_reserves_web::proof; + +fn main() { + let psbt = proof::decode_psbt(include_str!("../web/example/proof.psbt.base64")) + .expect("the fixture should be a valid PSBT"); + + // Input 0 is the challenge input, which has no UTXO behind it. + for txin in psbt.unsigned_tx.input.iter().skip(1) { + println!( + "{} {}", + txin.previous_output.txid, txin.previous_output.vout + ); + } +} diff --git a/patches/libc/Cargo.toml b/patches/libc/Cargo.toml new file mode 100644 index 0000000..64c7f91 --- /dev/null +++ b/patches/libc/Cargo.toml @@ -0,0 +1,22 @@ +# Stand-in for the `libc` crate, applied only to wasm32-unknown-unknown builds +# (see .cargo/wasm-patch.toml). The real libc has no definitions for that +# target, but `bitcoinconsensus` is the only crate in the wasm dependency graph +# that needs it, and all it wants are a few C integer type aliases that +# core::ffi provides. +# +# Native builds do not use this - they get the real libc. +[package] +name = "libc" +version = "0.2.189" +edition = "2021" +publish = false + +[lib] +name = "libc" +path = "src/lib.rs" + +[features] +default = ["std"] +std = [] +extra_traits = [] +align = [] diff --git a/patches/libc/src/lib.rs b/patches/libc/src/lib.rs new file mode 100644 index 0000000..1f09e2c --- /dev/null +++ b/patches/libc/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +pub use core::ffi::{c_char, c_int, c_long, c_schar, c_short, c_uchar, c_uint, c_ulong, c_ushort, c_void}; diff --git a/res/index.html b/res/index.html deleted file mode 100644 index f49c987..0000000 --- a/res/index.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - Bitcoin Proof of Reserves - - - - - -

Bitcoin Proof of Reserves

- You can verify BIP-0127 Proof of Reserves PSBTs.
- Learn more at bdk-reserves or Richis blog
- -

Proof details

-
-

- - -

-

- - -

-

- - -

-

- -

-
-

- Spendable: sats -

-
- - \ No newline at end of file diff --git a/src/csupport.rs b/src/csupport.rs new file mode 100644 index 0000000..65e57c9 --- /dev/null +++ b/src/csupport.rs @@ -0,0 +1,179 @@ +//! C runtime symbols that `bitcoinconsensus` needs on wasm. +//! +//! `bitcoinconsensus` compiles Bitcoin Core's script interpreter, so the build +//! needs a C++ toolchain and standard library for wasm. build.sh supplies those +//! from the wasi-sdk, but deliberately does *not* link wasi-libc: that would put +//! a second allocator in the module alongside Rust's and add a WASI import +//! surface the browser would have to fill in. +//! +//! Instead libc++ is backed by the handful of symbols below. `malloc`/`free` +//! forward to Rust's allocator so there is exactly one heap, and the rest are +//! either real implementations or inert stubs on paths this module never +//! reaches. The result imports nothing but the wasm-bindgen glue. + +use core::ffi::{c_char, c_int, c_void}; +use std::alloc::{alloc, dealloc, Layout}; + +/// Size header stored in front of every allocation. 16 bytes both records the +/// layout `free` has to reconstruct and keeps the returned pointer aligned to +/// wasm32's `max_align_t`. +const HDR: usize = 16; + +#[no_mangle] +pub unsafe extern "C" fn malloc(size: usize) -> *mut c_void { + let total = match size.checked_add(HDR) { + Some(total) => total, + None => return core::ptr::null_mut(), + }; + let layout = match Layout::from_size_align(total, HDR) { + Ok(layout) => layout, + Err(_) => return core::ptr::null_mut(), + }; + let base = alloc(layout); + if base.is_null() { + return core::ptr::null_mut(); + } + (base as *mut usize).write(total); + base.add(HDR) as *mut c_void +} + +#[no_mangle] +pub unsafe extern "C" fn calloc(count: usize, size: usize) -> *mut c_void { + let bytes = match count.checked_mul(size) { + Some(bytes) => bytes, + None => return core::ptr::null_mut(), + }; + let ptr = malloc(bytes); + if !ptr.is_null() { + core::ptr::write_bytes(ptr as *mut u8, 0, bytes); + } + ptr +} + +#[no_mangle] +pub unsafe extern "C" fn realloc(ptr: *mut c_void, size: usize) -> *mut c_void { + if ptr.is_null() { + return malloc(size); + } + if size == 0 { + free(ptr); + return core::ptr::null_mut(); + } + let base = (ptr as *mut u8).sub(HDR); + let old_total = (base as *mut usize).read(); + let new = malloc(size); + if new.is_null() { + return core::ptr::null_mut(); + } + let keep = core::cmp::min(old_total - HDR, size); + core::ptr::copy_nonoverlapping(ptr as *const u8, new as *mut u8, keep); + free(ptr); + new +} + +#[no_mangle] +pub unsafe extern "C" fn free(ptr: *mut c_void) { + if ptr.is_null() { + return; + } + let base = (ptr as *mut u8).sub(HDR); + let total = (base as *mut usize).read(); + dealloc(base, Layout::from_size_align_unchecked(total, HDR)); +} + +#[no_mangle] +pub unsafe extern "C" fn strcmp(a: *const c_char, b: *const c_char) -> c_int { + let (mut a, mut b) = (a as *const u8, b as *const u8); + loop { + let (x, y) = (*a, *b); + if x != y { + return x as c_int - y as c_int; + } + if x == 0 { + return 0; + } + a = a.add(1); + b = b.add(1); + } +} + +#[no_mangle] +pub extern "C" fn abort() -> ! { + core::arch::wasm32::unreachable() +} + +#[no_mangle] +pub extern "C" fn __assert_fail( + _expr: *const c_char, + _file: *const c_char, + _line: c_int, + _func: *const c_char, +) -> ! { + core::arch::wasm32::unreachable() +} + +/// Static destructors get registered but never run: the module lives as long as +/// the page does, so there is nothing to tear down. +#[no_mangle] +pub extern "C" fn __cxa_atexit(_f: *mut c_void, _arg: *mut c_void, _dso: *mut c_void) -> c_int { + 0 +} + +/// The wasi-sdk ships libc++ and libc++abi built without exception support, so +/// the Itanium throw path is unresolved. bitcoinconsensus only reaches it +/// through tinyformat's `TINYFORMAT_ERROR`, which fires on malformed format +/// strings inside Bitcoin Core itself rather than on anything callers pass in, +/// so trapping is the honest behaviour. The slot exists because the compiler +/// constructs the exception object before handing it to `__cxa_throw`. +static mut EXCEPTION_SLOT: [u8; 256] = [0; 256]; + +#[no_mangle] +pub extern "C" fn __cxa_allocate_exception(_size: usize) -> *mut c_void { + core::ptr::addr_of_mut!(EXCEPTION_SLOT) as *mut c_void +} + +#[no_mangle] +pub extern "C" fn __cxa_free_exception(_ptr: *mut c_void) {} + +#[no_mangle] +pub extern "C" fn __cxa_throw(_ex: *mut c_void, _ty: *mut c_void, _dt: *mut c_void) -> ! { + core::arch::wasm32::unreachable() +} + +// libc++ pulls in stream error reporting that nothing here can reach, since the +// module never writes to a stream. Keep the symbols inert rather than +// implementing stdio. +#[no_mangle] +pub static mut stderr: *mut c_void = core::ptr::null_mut(); + +#[no_mangle] +pub extern "C" fn fprintf(_stream: *mut c_void, _fmt: *const c_char, _args: *mut c_void) -> c_int { + 0 +} + +#[no_mangle] +pub extern "C" fn vfprintf(_stream: *mut c_void, _fmt: *const c_char, _args: *mut c_void) -> c_int { + 0 +} + +#[no_mangle] +pub extern "C" fn fputc(_ch: c_int, _stream: *mut c_void) -> c_int { + -1 +} + +#[no_mangle] +pub extern "C" fn fwrite( + _buf: *const c_void, + _size: usize, + _count: usize, + _stream: *mut c_void, +) -> usize { + 0 +} + +static ERROR_STRING: [u8; 8] = *b"unknown\0"; + +#[no_mangle] +pub extern "C" fn strerror(_errnum: c_int) -> *mut c_char { + ERROR_STRING.as_ptr() as *mut c_char +} diff --git a/src/esplora.rs b/src/esplora.rs new file mode 100644 index 0000000..8e92458 --- /dev/null +++ b/src/esplora.rs @@ -0,0 +1,238 @@ +//! A small Esplora client built on the browser's `fetch`. +//! +//! The service this replaces talked to Electrum over TCP, which a page cannot +//! do. Esplora exposes the same information over plain HTTP with CORS enabled, +//! so the lookups can run in the tab with no server of our own in between. + +use bdk_wallet::bitcoin::{ + consensus::deserialize, hashes::Hash, Address, OutPoint, Transaction, Txid, +}; +use futures_util::{stream, StreamExt}; +use serde::Deserialize; +use std::collections::HashMap; +use std::str::FromStr; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use web_sys::{Request, RequestInit, Response}; + +/// Look up `fetch` on the global object rather than going through `Window`, so +/// the same module works in a page, in a worker, and under Node. +fn fetch(request: &Request) -> Result { + let global = js_sys::global(); + let fetch = js_sys::Reflect::get(&global, &JsValue::from_str("fetch")) + .map_err(|_| "This environment provides no fetch()".to_string())?; + let fetch: js_sys::Function = fetch + .dyn_into() + .map_err(|_| "This environment provides no fetch()".to_string())?; + fetch + .call1(&global, request) + .map_err(|e| format!("Call to fetch() failed: {}", describe(&e)))? + .dyn_into() + .map_err(|_| "fetch() did not return a promise".to_string()) +} + +/// How many transaction fetches to keep in flight. Enough to hide latency on a +/// proof covering many UTXOs, low enough to stay polite to a public server. +const MAX_CONCURRENT_REQUESTS: usize = 8; + +pub struct Esplora { + base_url: String, +} + +#[derive(Debug, Deserialize)] +struct Utxo { + txid: String, + vout: u32, + status: UtxoStatus, +} + +#[derive(Debug, Deserialize)] +struct UtxoStatus { + confirmed: bool, + block_height: Option, +} + +impl Esplora { + pub fn new(base_url: &str) -> Self { + Self { + base_url: base_url.trim_end_matches('/').to_string(), + } + } + + async fn get(&self, path: &str) -> Result { + let url = format!("{}{}", self.base_url, path); + + let opts = RequestInit::new(); + opts.set_method("GET"); + + let request = Request::new_with_str_and_init(&url, &opts) + .map_err(|e| format!("Could not build a request for {}: {}", url, describe(&e)))?; + + let response: Response = JsFuture::from(fetch(&request)?) + .await + .map_err(|e| format!("Request to {} failed: {}", url, describe(&e)))? + .dyn_into() + .map_err(|_| format!("Unexpected response type from {}", url))?; + + if !response.ok() { + return Err(format!( + "{} returned HTTP {} {}", + url, + response.status(), + response.status_text() + )); + } + + let text = + JsFuture::from(response.text().map_err(|e| { + format!("Could not read the response from {}: {}", url, describe(&e)) + })?) + .await + .map_err(|e| format!("Could not read the response from {}: {}", url, describe(&e)))?; + + text.as_string() + .ok_or_else(|| format!("Response from {} was not text", url)) + } + + pub async fn tip_height(&self) -> Result { + let body = self.get("/blocks/tip/height").await?; + body.trim() + .parse() + .map_err(|e| format!("Could not read the chain tip height {:?}: {:?}", body, e)) + } + + /// Fetch the confirmed, sufficiently deep UTXOs of `address`. + /// + /// Only the outpoints are taken from this response. The amounts come from + /// the transactions themselves in `outpoints_for_addresses`, so a server + /// that misreports a value cannot inflate the total. + async fn confirmed_outpoints( + &self, + address: &Address, + max_confirmation_height: usize, + ) -> Result, String> { + let body = self.get(&format!("/address/{}/utxo", address)).await?; + let utxos: Vec = serde_json::from_str(&body) + .map_err(|e| format!("Could not read the UTXOs of {}: {:?}", address, e))?; + + utxos + .into_iter() + .filter(|utxo| { + utxo.status.confirmed + && utxo + .status + .block_height + .is_some_and(|height| height > 0 && height <= max_confirmation_height) + }) + .map(|utxo| { + Ok(OutPoint { + txid: Txid::from_str(&utxo.txid) + .map_err(|e| format!("Invalid txid {}: {:?}", utxo.txid, e))?, + vout: utxo.vout, + }) + }) + .collect() + } + + async fn transaction(&self, txid: Txid) -> Result { + let hex = self.get(&format!("/tx/{}/hex", txid)).await?; + let raw = hex_decode(hex.trim()) + .map_err(|e| format!("Transaction {} came back malformed: {}", txid, e))?; + let tx: Transaction = deserialize(&raw) + .map_err(|e| format!("Could not parse transaction {}: {:?}", txid, e))?; + + // The server told us this outpoint is unspent; check it actually handed + // back the transaction we asked for rather than taking its word for it. + if tx.compute_txid() != txid { + return Err(format!( + "Server returned a transaction whose txid is {} but {} was requested", + tx.compute_txid(), + txid + )); + } + + Ok(tx) + } + + /// Collect every spendable UTXO across `addresses`, paired with the output + /// it refers to, ready to hand to `verify_proof`. + pub async fn outpoints_for_addresses( + &self, + addresses: &[Address], + max_confirmation_height: usize, + ) -> Result, String> { + let per_address: Vec, String>> = stream::iter(addresses) + .map(|address| self.confirmed_outpoints(address, max_confirmation_height)) + .buffer_unordered(MAX_CONCURRENT_REQUESTS) + .collect() + .await; + + let outpoints: Vec = per_address + .into_iter() + .collect::, String>>()? + .concat(); + + // Several outpoints often share a transaction, so fetch each one once. + let mut wanted: Vec = outpoints.iter().map(|outpoint| outpoint.txid).collect(); + wanted.sort_unstable_by_key(|txid| txid.to_byte_array()); + wanted.dedup(); + + let fetched: Vec> = stream::iter(wanted) + .map(|txid| async move { self.transaction(txid).await.map(|tx| (txid, tx)) }) + .buffer_unordered(MAX_CONCURRENT_REQUESTS) + .collect() + .await; + + let transactions: HashMap = + fetched.into_iter().collect::>()?; + + outpoints + .into_iter() + .map(|outpoint| { + let tx = transactions + .get(&outpoint.txid) + .ok_or_else(|| format!("Missing transaction {}", outpoint.txid))?; + let txout = tx + .output + .get(outpoint.vout as usize) + .ok_or_else(|| { + format!( + "Transaction {} has no output {}", + outpoint.txid, outpoint.vout + ) + })? + .clone(); + Ok((outpoint, txout)) + }) + .collect() + } +} + +fn hex_decode(hex: &str) -> Result, String> { + let mut digits = hex.as_bytes().chunks_exact(2); + if !digits.remainder().is_empty() { + return Err("odd number of hex digits".to_string()); + } + digits + .by_ref() + .map(|pair| { + let pair = std::str::from_utf8(pair).map_err(|e| format!("{:?}", e))?; + u8::from_str_radix(pair, 16).map_err(|e| format!("{:?}", e)) + }) + .collect() +} + +/// Turn a thrown JS value into something worth showing a user. +fn describe(value: &JsValue) -> String { + value + .as_string() + .or_else(|| { + value + .dyn_ref::() + .map(|error| String::from(error.message())) + }) + .unwrap_or_else(|| { + "the request was blocked or the server is unreachable (check CORS and the URL)" + .to_string() + }) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fd70ad1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,115 @@ +//! Browser-side verifier for BIP-127 proof of reserves PSBTs. +//! +//! This used to be an HTTP service that verified proofs on behalf of whoever +//! asked. Everything it did now runs in the page instead: the PSBT never leaves +//! the browser, and the only network traffic is the UTXO lookups against an +//! Esplora server of the user's choosing. + +pub mod proof; + +#[cfg(target_arch = "wasm32")] +mod csupport; + +#[cfg(target_arch = "wasm32")] +mod esplora; + +#[cfg(target_arch = "wasm32")] +mod bindings { + use crate::{esplora::Esplora, proof}; + use bdk_wallet::bitcoin::Network; + use serde::{Deserialize, Serialize}; + use wasm_bindgen::prelude::*; + + /// The depth the HTTP service asked for, and a sensible default here too. + const DEFAULT_CONFIRMATIONS: usize = 3; + + #[derive(Debug, Deserialize)] + pub struct VerifyRequest { + addresses: Vec, + message: String, + proof_psbt: String, + #[serde(default)] + confirmations: Option, + #[serde(default)] + esplora_url: Option, + } + + #[derive(Debug, Serialize)] + pub struct VerifyResponse { + /// Total value the proof demonstrates control of, in satoshis. + spendable: u64, + network: String, + tip_height: usize, + /// How many UTXOs were deep enough to count towards the total. + utxos: usize, + esplora_url: String, + } + + #[wasm_bindgen(start)] + pub fn start() { + console_error_panic_hook::set_once(); + } + + /// Default Esplora endpoint for a network, used when the caller does not + /// name one. These are Blockstream's public servers, which send CORS + /// headers; any other Esplora instance works just as well. + fn default_esplora_url(network: Network) -> Result<&'static str, String> { + match network { + Network::Bitcoin => Ok("https://blockstream.info/api"), + Network::Testnet => Ok("https://blockstream.info/testnet/api"), + Network::Signet => Ok("https://blockstream.info/signet/api"), + other => Err(format!( + "No public Esplora server is known for {}, please give one explicitly", + other + )), + } + } + + /// Verify a proof of reserves. + /// + /// Resolves to a `VerifyResponse`, or rejects with a message describing why + /// the proof was not accepted. + #[wasm_bindgen(js_name = verifyProofOfReserves)] + pub async fn verify_proof_of_reserves(request: JsValue) -> Result { + run(request).await.map_err(|e| JsValue::from_str(&e)) + } + + async fn run(request: JsValue) -> Result { + let request: VerifyRequest = serde_wasm_bindgen::from_value(request) + .map_err(|e| format!("Could not read the request: {}", e))?; + + let addresses: Vec = request + .addresses + .iter() + .map(|address| address.trim().to_string()) + .filter(|address| !address.is_empty()) + .collect(); + + let psbt = proof::decode_psbt(&request.proof_psbt)?; + let network = proof::detect_network(&addresses)?; + let parsed = proof::parse_addresses(&addresses, network)?; + + let esplora_url = match request.esplora_url.as_deref().map(str::trim) { + Some(url) if !url.is_empty() => url.to_string(), + _ => default_esplora_url(network)?.to_string(), + }; + let esplora = Esplora::new(&esplora_url); + + let confirmations = request.confirmations.unwrap_or(DEFAULT_CONFIRMATIONS); + let tip_height = esplora.tip_height().await?; + let cutoff = proof::confirmation_cutoff(tip_height, confirmations)?; + + let outpoints = esplora.outpoints_for_addresses(&parsed, cutoff).await?; + let utxos = outpoints.len(); + let spendable = proof::verify(&psbt, &request.message, outpoints)?; + + serde_wasm_bindgen::to_value(&VerifyResponse { + spendable: spendable.to_sat(), + network: network.to_string(), + tip_height, + utxos, + esplora_url, + }) + .map_err(|e| format!("Could not build the response: {}", e)) + } +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 53ead27..0000000 --- a/src/main.rs +++ /dev/null @@ -1,184 +0,0 @@ -use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer, Responder}; -use bdk_electrum::{ - electrum_client::{self, ElectrumApi}, - BdkElectrumClient, -}; -use bdk_reserves::reserves::verify_proof; -use bdk_wallet::bitcoin::{ - base64::{engine::general_purpose::STANDARD, Engine as _}, - psbt::Psbt, - {Address, Network, OutPoint, TxOut}, -}; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use std::{env, io, str::FromStr}; - -#[derive(Debug, Serialize, Deserialize)] -struct ProofOfReserves { - addresses: Vec, - message: String, - proof_psbt: String, -} - -#[actix_web::main] -async fn main() -> io::Result<()> { - let address = env::var("BIND_ADDRESS").unwrap_or_else(|_err| match env::var("PORT") { - Ok(p) => format!("0.0.0.0:{}", p), - Err(_e) => "localhost:8087".to_string(), - }); - - println!("Starting HTTP server at http://{}.", address); - println!("You can choose a different address through the BIND_ADDRESS env var."); - println!("You can choose a different port through the PORT env var."); - - HttpServer::new(|| { - App::new() - .wrap(middleware::Logger::default()) // <- enable logger - .app_data(web::JsonConfig::default().limit(40960)) // <- limit size of the payload (global configuration) - .service(web::resource("/proof").route(web::post().to(check_proof))) - .service(index) - }) - .bind(address)? - .run() - .await -} - -#[get("/")] -async fn index() -> impl Responder { - let html = include_str!("../res/index.html"); - HttpResponse::Ok().content_type("text/html").body(html) -} - -async fn check_proof(item: web::Json, req: HttpRequest) -> HttpResponse { - println!("request: {:?}", req); - println!("model: {:?}", item); - - let proof_result = - handle_ext_reserves(&item.message, &item.proof_psbt, 3, item.addresses.clone()); - - let answer = match proof_result { - Err(e) => json!({ "error": e }), - Ok(res) => res, - } - .to_string(); - HttpResponse::Ok().content_type("text/json").body(answer) -} - -fn handle_ext_reserves( - message: &str, - psbt: &str, - confirmations: usize, - addresses: Vec, -) -> Result { - let psbt = STANDARD - .decode(psbt) - .map_err(|e| format!("Base64 decode error: {:?}", e))?; - let psbt = - Psbt::deserialize(&psbt).map_err(|e| format!("PSBT deserialization error: {:?}", e))?; - if addresses.is_empty() { - return Err("No address provided".to_string()); - } - let (server, network) = if addresses[0].starts_with('2') { - ("ssl://electrum.blockstream.info:60002", Network::Testnet) - } else { - ("ssl://electrum.blockstream.info:50002", Network::Bitcoin) - }; - - let client = electrum_client::Client::new(server) - .map_err(|e| format!("Failed to create Electrum client: {:?}", e))?; - let client = BdkElectrumClient::new(client); - - let current_block_height = client - .inner - .block_headers_subscribe() - .map(|data| data.height) - .map_err(|e| format!("Failed to get block height: {:?}", e))?; - let max_confirmation_height = Some(current_block_height - confirmations); - - let outpoints_per_addr = addresses - .iter() - .map(|address| { - let address = Address::from_str(address) - .map_err(|e| format!("Invalid address: {:?}", e))? - .require_network(network) - .map_err(|e| format!("Invalid address: {:?}", e))?; - get_outpoints_for_address(&address, &client, max_confirmation_height) - }) - .collect::>, String>>()?; - let outpoints_combined = outpoints_per_addr - .iter() - .fold(Vec::new(), |mut outpoints, outs| { - outpoints.append(&mut outs.clone()); - outpoints - }); - - let spendable = - verify_proof(&psbt, message, outpoints_combined).map_err(|e| format!("{:?}", e))?; - - Ok(json!({ "spendable": spendable })) -} - -/// Fetch all the utxos, for a given address. -fn get_outpoints_for_address( - address: &Address, - client: &BdkElectrumClient, - max_confirmation_height: Option, -) -> Result, String> { - let unspents = client - .inner - .script_list_unspent(&address.script_pubkey()) - .map_err(|e| format!("{:?}", e))?; - - unspents - .iter() - .filter(|utxo| { - utxo.height > 0 && utxo.height <= max_confirmation_height.unwrap_or(usize::MAX) - }) - .map(|utxo| { - let tx = match client.inner.transaction_get(&utxo.tx_hash) { - Ok(tx) => tx, - Err(e) => { - return Err(e).map_err(|e| format!("{:?}", e))?; - } - }; - - Ok(( - OutPoint { - txid: utxo.tx_hash, - vout: utxo.tx_pos as u32, - }, - tx.output[utxo.tx_pos].clone(), - )) - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use actix_web::{body::to_bytes, dev::Service, http, test, web, App, Error}; - - #[actix_web::test] - async fn test_index() -> Result<(), Error> { - let app = App::new().route("/proof", web::post().to(check_proof)); - let app = test::init_service(app).await; - - let req = test::TestRequest::post().uri("/proof") - .set_json(ProofOfReserves { - addresses: vec!["2Mtkk3kjyN8hgdGXPuJCNnwS3BBY4K2frhY".to_owned()], - message: "Stored in SEBA Bank AG cold storage".to_owned(), - proof_psbt: "cHNidP8BAP03AwEAAAATfUqjtTyZAxfGOsqFi93k3ToGtPZ0E/BZWFlBgAFlt1kAAAAAAP////8VZFle1kNhN87Ee3jTlpqzhPY3376Bee8gryZ4EP0QxQAAAAAA/////xdqWOlIfYFpbDM+ZuBHu05GiQz+EKK/ebafYy50BPwqAAAAAAD/////K6q1ppFH2Ai6FYgXhqAP/i25RVrCNl7/LKkDKAfBedkAAAAAAP////8rqrWmkUfYCLoViBeGoA/+LblFWsI2Xv8sqQMoB8F52QEAAAAA/////yvmR/yPrZNvLPEWPdteixrpIrSe+mjGV0PRHwQvJ3skAAAAAAD/////cuwKmKQFtYW/+/3y8/ePnheAut3yDHv0R7HV22UhJX0AAAAAAP////9y7AqYpAW1hb/7/fLz94+eF4C63fIMe/RHsdXbZSElfQEAAAAA/////4ygvq0AS059XinGKxwy8SqKjRANTF6dU+CDPXemeDqVAAAAAAD/////jKC+rQBLTn1eKcYrHDLxKoqNEA1MXp1T4IM9d6Z4OpUBAAAAAP////+3xGKCPa4t1MGlkJ9jznWYBGdP9XZNMKbW+t7UvnNxzAAAAAAA/////7fEYoI9ri3UwaWQn2POdZgEZ0/1dk0wptb63tS+c3HMAQAAAAD/////wyKNGqQJpgaNszr5mLLEYQV6+lAMfXNndS/mn8PkXJ0AAAAAAP/////DIo0apAmmBo2zOvmYssRhBXr6UAx9c2d1L+afw+RcnQEAAAAA/////9Fninwz/x77J2ghJX0wcVNLRI3f3wMIlh5kePz8l2ZuAAAAAAD/////1AUOKakFoN1BqrDomHASI0VFsLtskXVQpPljoDU8zWsBAAAAAP/////swob+WCNq5562PWB+Z5JOFFogd/20GAr4Vyra6oOIAAAAAAAA/////+zChv5YI2rnnrY9YH5nkk4UWiB3/bQYCvhXKtrqg4gAAQAAAAD/////8pkjhcQSFD62iDk1sC4WLBUPcpKNoeup0O98xe4MF+kAAAAAAP////8BbOw1AwAAAAAZdqkUn3/QltN+0sDj9/DPySS+70/862iIrAAAAAAAAQEKAAAAAAAAAAABUQEHAAABASAoOPwCAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDore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".to_owned(), - }) - .to_request(); - let resp = app.call(req).await?; - - assert_eq!(resp.status(), http::StatusCode::OK); - - let response_body = resp.into_body(); - let resp = r#"{"error":"NonSpendableInput(1)"}"#; - assert_eq!(to_bytes(response_body).await?, resp); - //assert_eq!(to_bytes(response_body).await?, r##"Hello world!"##); - - Ok(()) - } -} diff --git a/src/proof.rs b/src/proof.rs new file mode 100644 index 0000000..48a56f6 --- /dev/null +++ b/src/proof.rs @@ -0,0 +1,194 @@ +//! The parts of proof verification that do not touch the network. +//! +//! Keeping these separate from the Esplora client means they can be unit tested +//! with a plain `cargo test` on the host, without a browser or a live server. + +use bdk_reserves::reserves::verify_proof; +use bdk_wallet::bitcoin::{ + base64::{engine::general_purpose::STANDARD, Engine as _}, + psbt::Psbt, + Address, Amount, Network, OutPoint, TxOut, +}; +use std::str::FromStr; + +/// Networks we are willing to resolve an address against, most likely first. +const SUPPORTED_NETWORKS: [Network; 4] = [ + Network::Bitcoin, + Network::Testnet, + Network::Signet, + Network::Regtest, +]; + +pub fn decode_psbt(proof_psbt: &str) -> Result { + let raw = STANDARD + .decode(proof_psbt.trim()) + .map_err(|e| format!("Base64 decode error: {:?}", e))?; + Psbt::deserialize(&raw).map_err(|e| format!("PSBT deserialization error: {:?}", e)) +} + +/// Work out which network the proof is for by seeing which one every address +/// parses against. +/// +/// The addresses themselves are the only hint available, so rather than guess +/// from a prefix character we take the first network that accepts all of them. +/// Mainnet and testnet share `tb1`/`bcrt1`-style ambiguity in places, hence the +/// fixed precedence in `SUPPORTED_NETWORKS`. +pub fn detect_network(addresses: &[String]) -> Result { + if addresses.is_empty() { + return Err("No address provided".to_string()); + } + + let unchecked = addresses + .iter() + .map(|address| { + Address::from_str(address.trim()) + .map_err(|e| format!("Invalid address {}: {:?}", address, e)) + }) + .collect::, String>>()?; + + SUPPORTED_NETWORKS + .into_iter() + .find(|network| { + unchecked + .iter() + .all(|address| address.is_valid_for_network(*network)) + }) + .ok_or_else(|| { + "The addresses do not all belong to the same network, or the network is not supported" + .to_string() + }) +} + +pub fn parse_addresses(addresses: &[String], network: Network) -> Result, String> { + addresses + .iter() + .map(|address| { + Address::from_str(address.trim()) + .map_err(|e| format!("Invalid address {}: {:?}", address, e))? + .require_network(network) + .map_err(|e| format!("Address {} is not valid on {}: {:?}", address, network, e)) + }) + .collect() +} + +/// Highest block height a UTXO may sit at and still count as confirmed. +/// +/// A UTXO mined in block `h` has `tip - h + 1` confirmations, the block it +/// landed in being the first, so asking for at least `confirmations` of them +/// means `h <= tip + 1 - confirmations`. +pub fn confirmation_cutoff(tip_height: usize, confirmations: usize) -> Result { + if confirmations > tip_height { + return Err(format!( + "The chain is only {} blocks long, which cannot satisfy {} confirmations", + tip_height, confirmations + )); + } + Ok(tip_height + 1 - confirmations) +} + +/// Verify the proof against the UTXOs that were found to be unspent. +pub fn verify( + psbt: &Psbt, + message: &str, + outpoints: Vec<(OutPoint, TxOut)>, +) -> Result { + verify_proof(psbt, message, outpoints).map_err(|e| format!("{:?}", e)) +} + +#[cfg(test)] +mod tests { + use super::*; + + // A real proof, the same one the HTTP service used as its test fixture. + const PROOF_PSBT: &str = include_str!("../web/example/proof.psbt.base64"); + const MESSAGE: &str = "Stored in SEBA Bank AG cold storage"; + const ADDRESS: &str = "2Mtkk3kjyN8hgdGXPuJCNnwS3BBY4K2frhY"; + + #[test] + fn decodes_a_real_proof() { + let psbt = decode_psbt(PROOF_PSBT).expect("fixture should decode"); + // A BIP-127 proof is the challenge input plus one input per UTXO, + // collapsing into a single unspendable output. + assert!(psbt.inputs.len() > 1); + assert_eq!(psbt.unsigned_tx.output.len(), 1); + } + + #[test] + fn rejects_malformed_base64() { + assert!(decode_psbt("not a psbt").is_err()); + } + + #[test] + fn detects_testnet_from_a_p2sh_address() { + let addresses = vec![ADDRESS.to_string()]; + assert_eq!(detect_network(&addresses).unwrap(), Network::Testnet); + } + + #[test] + fn detects_mainnet_from_a_bech32_address() { + let addresses = vec!["bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string()]; + assert_eq!(detect_network(&addresses).unwrap(), Network::Bitcoin); + } + + #[test] + fn refuses_addresses_from_different_networks() { + let addresses = vec![ + "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string(), + ADDRESS.to_string(), + ]; + assert!(detect_network(&addresses).is_err()); + } + + #[test] + fn refuses_an_empty_address_list() { + assert!(detect_network(&[]).is_err()); + } + + #[test] + fn parses_addresses_for_the_detected_network() { + let addresses = vec![ADDRESS.to_string()]; + let network = detect_network(&addresses).unwrap(); + let parsed = parse_addresses(&addresses, network).unwrap(); + assert_eq!(parsed.len(), 1); + } + + /// The block a UTXO lands in counts as its first confirmation, so at a tip + /// of 100 the shallowest block with 3 confirmations is 98, not 97. + #[test] + fn confirmation_cutoff_counts_the_including_block() { + assert_eq!(confirmation_cutoff(100, 3).unwrap(), 98); + } + + #[test] + fn one_confirmation_admits_the_tip_block() { + assert_eq!(confirmation_cutoff(100, 1).unwrap(), 100); + } + + #[test] + fn zero_confirmations_admits_anything_already_mined() { + assert_eq!(confirmation_cutoff(100, 0).unwrap(), 101); + } + + #[test] + fn confirmation_cutoff_refuses_a_depth_the_chain_cannot_reach() { + assert!(confirmation_cutoff(2, 3).is_err()); + assert!(confirmation_cutoff(2, 5).is_err()); + } + + /// Without the UTXOs backing it, a proof has to be rejected rather than + /// quietly reporting a smaller balance. This is what the HTTP service's + /// test asserted, except it needed a live Electrum server to get there. + #[test] + fn a_proof_with_no_matching_utxos_is_not_spendable() { + let psbt = decode_psbt(PROOF_PSBT).unwrap(); + let err = verify(&psbt, MESSAGE, Vec::new()).unwrap_err(); + assert_eq!(err, "NonSpendableInput(1)"); + } + + #[test] + fn a_proof_checked_against_the_wrong_message_is_rejected() { + let psbt = decode_psbt(PROOF_PSBT).unwrap(); + let err = verify(&psbt, "some other message", Vec::new()).unwrap_err(); + assert_eq!(err, "ChallengeInputMismatch"); + } +} diff --git a/tests/e2e.cjs b/tests/e2e.cjs new file mode 100644 index 0000000..a86cb11 --- /dev/null +++ b/tests/e2e.cjs @@ -0,0 +1,162 @@ +// End-to-end test of the built wasm module, driven through its real JS API +// against a mock Esplora server. +// +// The fixture proof is genuine and so are its signatures, but the UTXOs it +// covers were spent years ago, so a live server reports nothing unspent. +// Serving the real transactions back as though they were still unspent +// exercises the whole success path, consensus signature checks on all 18 inputs +// included, without needing a wallet or a regtest node. +// +// Run it with `make test-e2e`, which builds the module first. + +const http = require('http') +const fs = require('fs') +const path = require('path') + +const FIXTURES = path.join(__dirname, 'fixtures') +const EXAMPLE = path.join(__dirname, '..', 'web', 'example') + +// Read the same metadata the "Load example" button loads, so the example the +// site offers and the proof this test asserts on cannot drift apart. +const example = JSON.parse(fs.readFileSync(path.join(EXAMPLE, 'proof.json'), 'utf8')) +const ADDRESS = example.addresses[0] +const MESSAGE = example.message +const PSBT = fs.readFileSync(path.join(EXAMPLE, example.proof_psbt), 'utf8').trim() +const TIP = 2000000 +const EXPECTED_SATS = 53865580 +const DEPTH_BELOW_TIP = 100 +// The including block is the first confirmation, hence the +1. +const DEPTH = DEPTH_BELOW_TIP + 1 + +const wasm = require(path.join(__dirname, '..', 'target', 'e2e', 'bdk_reserves_web.js')) + +const outpoints = fs + .readFileSync(path.join(FIXTURES, 'outpoints.txt'), 'utf8') + .trim() + .split('\n') + .map((line) => { + const [txid, vout] = line.split(' ') + return { txid, vout: Number(vout) } + }) + +const transactions = {} +for (const txid of fs.readdirSync(path.join(FIXTURES, 'transactions'))) { + transactions[txid] = fs.readFileSync(path.join(FIXTURES, 'transactions', txid), 'utf8').trim() +} + +const served = { tip: 0, utxo: 0, tx: 0 } + +const server = http.createServer((req, res) => { + const send = (code, body) => { + res.writeHead(code, { 'content-type': 'text/plain' }) + res.end(body) + } + + if (req.url === '/blocks/tip/height') { + served.tip++ + return send(200, String(TIP)) + } + + let match + if ((match = req.url.match(/^\/address\/([^/]+)\/utxo$/))) { + served.utxo++ + if (match[1] !== ADDRESS) return send(200, '[]') + return send( + 200, + JSON.stringify( + outpoints.map((outpoint) => ({ + ...outpoint, + status: { confirmed: true, block_height: TIP - DEPTH_BELOW_TIP }, + })), + ), + ) + } + + if ((match = req.url.match(/^\/tx\/([0-9a-f]{64})\/hex$/))) { + served.tx++ + return transactions[match[1]] ? send(200, transactions[match[1]]) : send(404, 'Not found') + } + + send(404, 'Not found') +}) + +const verify = (overrides = {}) => + wasm.verifyProofOfReserves({ + addresses: [ADDRESS], + message: MESSAGE, + proof_psbt: PSBT, + confirmations: 3, + esplora_url: `http://127.0.0.1:${server.address().port}`, + ...overrides, + }) + +let failures = 0 +const check = (name, ok, detail) => { + console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' -> ' + detail : ''}`) + if (!ok) failures++ +} + +const rejects = async (name, overrides, expected) => { + try { + await verify(overrides) + check(name, false, 'the proof was accepted') + } catch (error) { + check(name, String(error).includes(expected), String(error)) + } +} + +server.listen(0, '127.0.0.1', async () => { + try { + check('the example lists one address', example.addresses.length === 1, String(example.addresses.length)) + check('the example warns its utxos are spent', /NonSpendableInput/.test(example.note), example.note.slice(0, 40) + '…') + + const proof = await verify() + check('a valid proof verifies', proof.spendable === EXPECTED_SATS, `${proof.spendable} sats`) + check('the network is detected', proof.network === 'testnet', proof.network) + check('the chain tip is reported', proof.tip_height === TIP, String(proof.tip_height)) + check('every outpoint is counted', proof.utxos === outpoints.length, String(proof.utxos)) + check('transaction fetches are deduped', served.tx === 12, `${served.tx} requests for 18 utxos`) + + await rejects('a proof is bound to its message', { message: 'another message' }, 'ChallengeInputMismatch') + + // A spent UTXO has to fail the proof outright rather than quietly lowering + // the total, which is the whole point of the exercise. + const removed = outpoints.splice(0, 1) + await rejects('a spent utxo fails the proof', {}, 'NonSpendableInput') + outpoints.unshift(...removed) + + await rejects('confirmation depth is enforced', { confirmations: 200 }, 'NonSpendableInput') + + // The fixture UTXOs sit 100 blocks below the tip, so they have exactly 101 + // confirmations: the including block counts as the first. These two pin + // that boundary down from both sides. + const atTheLimit = await verify({ confirmations: DEPTH }) + check('the exact confirmation depth is accepted', atTheLimit.utxos === outpoints.length, `${DEPTH} confirmations`) + await rejects('one more confirmation than exists is refused', { confirmations: DEPTH + 1 }, 'NonSpendableInput') + await rejects('an empty address list is refused', { addresses: [] }, 'No address provided') + await rejects('a malformed psbt is refused', { proof_psbt: 'garbage!!' }, 'Base64 decode error') + await rejects( + 'addresses from different networks are refused', + { addresses: ['bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', ADDRESS] }, + 'same network', + ) + + // The amounts come from the transactions rather than the UTXO listing, so a + // server that swaps one in must be caught rather than believed. + const [first, second] = Object.keys(transactions) + const original = transactions[first] + transactions[first] = transactions[second] + await rejects('a substituted transaction is caught', {}, 'txid is') + transactions[first] = original + + const again = await verify() + check('a repeat run is stable', again.spendable === proof.spendable, `${again.spendable} sats`) + } catch (error) { + console.log('unexpected failure:', error) + failures++ + } + + server.close() + console.log(failures === 0 ? '\nAll checks passed.' : `\n${failures} check(s) failed.`) + process.exit(failures === 0 ? 0 : 1) +}) diff --git a/tests/fixtures/outpoints.txt b/tests/fixtures/outpoints.txt new file mode 100644 index 0000000..bd9db01 --- /dev/null +++ b/tests/fixtures/outpoints.txt @@ -0,0 +1,18 @@ +c510fd107826af20ef7981bedf37f684b39a96d3787bc4ce376143d65e596415 0 +2afc04742e639fb679bfa210fe0c89464ebb47e0663e336c69817d48e9586a17 0 +d979c1072803a92cff5e36c25a45b92dfe0fa086178815ba08d84791a6b5aa2b 0 +d979c1072803a92cff5e36c25a45b92dfe0fa086178815ba08d84791a6b5aa2b 1 +247b272f041fd14357c668fa9eb422e91a8b5edb3d16f12c6f93ad8ffc47e62b 0 +7d252165dbd5b147f47b0cf2ddba80179e8ff7f3f2fdfbbf85b505a4980aec72 0 +7d252165dbd5b147f47b0cf2ddba80179e8ff7f3f2fdfbbf85b505a4980aec72 1 +953a78a6773d83e0539d5e4c0d108d8a2af1321c2bc6295e7d4e4b00adbea08c 0 +953a78a6773d83e0539d5e4c0d108d8a2af1321c2bc6295e7d4e4b00adbea08c 1 +cc7173bed4defad6a6304d76f54f67049875ce639f90a5c1d42dae3d8262c4b7 0 +cc7173bed4defad6a6304d76f54f67049875ce639f90a5c1d42dae3d8262c4b7 1 +9d5ce4c39fe62f7567737d0c50fa7a0561c4b298f93ab38d06a609a41a8d22c3 0 +9d5ce4c39fe62f7567737d0c50fa7a0561c4b298f93ab38d06a609a41a8d22c3 1 +6e6697fcfc78641e960803dfdf8d444b5371307d25216827fb1eff337c8a67d1 0 +6bcd3c35a063f9a45075916cbbb0454523127098e8b0aa41dda005a9290e05d4 1 +008883eada2a57f80a18b4fd77205a144e92677e603db69ee76a2358fe86c2ec 0 +008883eada2a57f80a18b4fd77205a144e92677e603db69ee76a2358fe86c2ec 1 +e9170ceec57cefd0a9eba18d92720f152c162eb0353988b63e1412c4852399f2 0 diff --git a/tests/fixtures/transactions/008883eada2a57f80a18b4fd77205a144e92677e603db69ee76a2358fe86c2ec b/tests/fixtures/transactions/008883eada2a57f80a18b4fd77205a144e92677e603db69ee76a2358fe86c2ec new file mode 100644 index 0000000..d8ca181 --- /dev/null +++ b/tests/fixtures/transactions/008883eada2a57f80a18b4fd77205a144e92677e603db69ee76a2358fe86c2ec @@ -0,0 +1 @@ +010000000001027ee0e5ebeeb8c8c846770e870755dfcc1d74e9747112227a34d9ee758a370cd201000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff7ee0e5ebeeb8c8c846770e870755dfcc1d74e9747112227a34d9ee758a370cd200000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff02889203000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148790d003000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e14870500473044022003d78c4b682e5997c3032476141dfdb6e1706f8434109b4fe229afc08fc6abcb02200a8cf1b56fd6449f655827b80c85dee9e4aa1df631d03844db00f737fed2fb5801483045022100caff9d91b0a53dc7795506744721be05f329f35354ec4782e53d4958cefbedb902203c2dc2036198b0e2c40bb942e0536024957e18058e34315d40b61f616d0ddf0c0147304402201ce0426637f388c68f892e6c2228308a665f134f8de0351d444435e33ba36c91022044444e165e4c2c0ff9ebbff5e209d6aed610b9ae59c973acea058533fde0daf601f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae0500483045022100ba97ebed31af46fe671e5afe7a4d5fbde191178280c8b3104366b297a097c70102203e5986ddd78f896c26d17fb500bdc284af1a3301eb0961624b94e5ed3750e83b0147304402207747a506b89f72dce0b4c7309cc659e9ea174aa40b7ec34c208e8ec76dffd68f02206e40cdfe5a772c7355c8939cfed82f1182ef0dddd05b13a139099d967efd762801483045022100c640b841bb63fa5f95b1749a77019473a916d4432e795e1eb7725d0f49d45c87022029fd9df15680cbdc83b72f964f09567418bff2ad26fb59d51cee063d99e21c6301f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/247b272f041fd14357c668fa9eb422e91a8b5edb3d16f12c6f93ad8ffc47e62b b/tests/fixtures/transactions/247b272f041fd14357c668fa9eb422e91a8b5edb3d16f12c6f93ad8ffc47e62b new file mode 100644 index 0000000..e44c05a --- /dev/null +++ b/tests/fixtures/transactions/247b272f041fd14357c668fa9eb422e91a8b5edb3d16f12c6f93ad8ffc47e62b @@ -0,0 +1 @@ +0100000000010288c55c42535ec600b5a451e3229c6531e6143c4b396af94c869eadb6fdfea22000000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffffd4050e29a905a0dd41aab0e8987012234545b0bb6c917550a4f963a0353ccd6b00000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff0290d003000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148790ef02000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148705004730440220465e9412e6bc85246ca715dad810ade05cb5f60425a88a8dfb9ba3af80b39e0602205daaab4f3b5f60226633fb55b528968e6256c3b393c278ae701f77c753488bf501473044022071afbdc01f8000d265048c38e872cc58ca3ce1b7442fff9bc8014c95cb47a988022065e3f26a1e11ece08bee1d6407996852cedfe20aae310d50d54e6fa9c9d8418201473044022058cf3b00d677952ec6c91b5d2a81225df014caa5efb0f0ca022d606966f82c9c022016f9c92683f7f5c1d53902e897ddaf517ba5e520fcdd9c62fed625dd7659f77501f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae0500483045022100cbf2d6b69d50e1737a8bec8cf3105fdb2815b4af9dc8baf261e5d261436927bb022060e9bf043b5ec67d75d51fc6cfe7ee4b11e43b3a8c014b635b8cebcdb65236b501483045022100b0e24af1b1d3a8ff569724cf084397918af43773b20fc9a6472605804f0da550022069051f23a043fcdaff62b07cb88a963bcdae6260c0e5edb26c3c2f3fa9dcced101483045022100b794e2f919ab869dbb56f69e97d9bf09afcae1b21046428b2ba94af1bc60f77b02200e2706e75a7a936936d56662de4e3ac687e0c5caadf0cb49102e4b4918cb661c01f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/2afc04742e639fb679bfa210fe0c89464ebb47e0663e336c69817d48e9586a17 b/tests/fixtures/transactions/2afc04742e639fb679bfa210fe0c89464ebb47e0663e336c69817d48e9586a17 new file mode 100644 index 0000000..1a87b0b --- /dev/null +++ b/tests/fixtures/transactions/2afc04742e639fb679bfa210fe0c89464ebb47e0663e336c69817d48e9586a17 @@ -0,0 +1 @@ +01000000000102f9936564be20266037ff0c6679ebb86335f3606068edb9d47ee140884c8a2ff500000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff993fec07b72d59862c9800c2f5f3b5043c4a80dc02de186f3d8f32d22b8dd28001000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff0290d003000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e1487509803000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e14870500483045022100899454e82cb2caf1c6915c99bc690e04d541555350493c02b8094c27c4b6e0be0220372e4fee85b24f1f427fbf619f39990f66c62b8002f9585311c2ce4e4b03a00501473044022004c2331c077ca26815996f473c36d70c8d3974491f2513d70c89e288640101d70220073716d02e4f8884550f21d2777c8ae5f2a3d6b5ef7ea7a431cdc7a14ed928b4014730440220400e865826f7d68a9864c284a38089a7ab9c2a5abb88a01b5b87b377837507ab02204d219ea8b1f7344493b443a186f017c545a96c7b85f7fabc95fb3f15d6b2fcf401f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae0500473044022070f6344c0a14e7831b86ef2aa201fe039be378559907b61c6fca636e54516663022010783261ae6a562f520b044b91cb79833853d90648cbc6ffa31e867e4ec5781d0148304502210081952770be03f5b2c22777c6cead498fa3fcc8d1851477625048139169040be30220568bd7f24c50041704c64f1e2ff0b51d1dd64d4d91167d627d83593696ea503c0147304402201c575dd2a1763cdebba7fa3320a2f852075d0934573d6afe587d4a3510d7ab5c02203ed85ec6e4ba301d976e42ccc79bf12ad1aacaa67aa475754b941a0a7ddaa15801f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/6bcd3c35a063f9a45075916cbbb0454523127098e8b0aa41dda005a9290e05d4 b/tests/fixtures/transactions/6bcd3c35a063f9a45075916cbbb0454523127098e8b0aa41dda005a9290e05d4 new file mode 100644 index 0000000..0d44454 --- /dev/null +++ b/tests/fixtures/transactions/6bcd3c35a063f9a45075916cbbb0454523127098e8b0aa41dda005a9290e05d4 @@ -0,0 +1 @@ +0100000000010239b0ed4ca34e323e5921ad78be1d92d366e218c427d3dbf3ac148809d9b3536200000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffffd8d32a8f2f10077ecc4182ad0da3eaf6acb1858d5da6e02116b8f7e7820a07d200000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff0240bc03000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e1487400d03000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e14870500483045022100bb76c7ec3527fbb8c42533c4d441ffea6e38979cd161e65e13c5b0249da4c8a302207c67053e0c2a9d158cf896e088157626634db25d81bcd34996e9ec7f1c59eeb101473044022008fe593ffb107764baa15b9d18474a16eabee2dee6376eda06e3c82956f5d3c70220656b9a2caa54239ff1b0b527b81b3367eca3e2f9af04f15e50e293ffdb33df9001473044022025511d219643ad1bd8ab5678fb93cf4210d839de2a80e022041ccf535157ffd10220428b5fcdb829495172cbf2c2fb15b932e701de3ef7cab30ab2861a484cddac6801f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae0500483045022100f195f6295415b603246d1a80d36484a0c0493df6405a2404ac6893c69bdbb6a5022058e76721cf61c9c0ae9a7a74db15ef1b9bd6b4cfd6a6b9e2fc89efd639a336d601473044022029af9d2d375dc77a56444b551050815d8f631359deb3e9d87a38ac27349c744f02200f111716cb0e431fe7111f75a2c5a891ac5e081c1374602f05228db5917332c801473044022006863317a041270822bd39c8acf40d5713cf2a35a9600d0274657b909340061002200af324b341227692d0e894ff370e249beac12cabbecc8fca6c0b88830d741dd301f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/6e6697fcfc78641e960803dfdf8d444b5371307d25216827fb1eff337c8a67d1 b/tests/fixtures/transactions/6e6697fcfc78641e960803dfdf8d444b5371307d25216827fb1eff337c8a67d1 new file mode 100644 index 0000000..07564d7 --- /dev/null +++ b/tests/fixtures/transactions/6e6697fcfc78641e960803dfdf8d444b5371307d25216827fb1eff337c8a67d1 @@ -0,0 +1 @@ +010000000001028e4a5142041617eb97497dbb250750209e4f4fc6369037e0081002de6c3325d601000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff60ec7d2be2e11bf96de2591026086735a80e763bf644b912dc73205c254c3b5d00000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff0204e200000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148790d003000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148705004830450221008cf19fef52badb84ada407c1058c9f235e8af653301ca29b534aabffdd456de302206acd35b9a75f5b495afbfebc251eda56d724308853e6f025f576de78f5df04880147304402206a0de1370aa7514af8a6d51853bc923c405ecc64a01b94e09f2b4afe0c151cdd02203f25283187522263621d318fa3b1f6e62de32367fc5a9cc47e31780da264aa200148304502210081cc0f28b909e6bb117c2a1441f689a1798aa2ac1d092bf535a943dec2d3bca202206c7e0ad7cd1425195b0a393ef1d0321fc21604f7da4d455a85b3eba0de79accf01f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae0500483045022100c852cafb298d22fc9c0fe3f935a8d14434015646702f542f3e8df654eab4e1dc02201e28bd45f3fa81178af071311f9f5c98f079b892117377921ffaec223035d7ac01483045022100823b3ea2a722cf9dd543accb3c70aa0701be4dade93429aebae9b930719af1ba02204ad9d0dcca1d0bec3ea281d4404530b941f7e677497ed4c74aedc2ad3372ad4201483045022100903ef68ddb8fdcd0fcd330940fa69ae1a635eea134cf1ab7d5ecf86316c2958102205865000b573eea1d97644920f2c32ac2875a404ed489c90c2d05e6821d86bdf701f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/7d252165dbd5b147f47b0cf2ddba80179e8ff7f3f2fdfbbf85b505a4980aec72 b/tests/fixtures/transactions/7d252165dbd5b147f47b0cf2ddba80179e8ff7f3f2fdfbbf85b505a4980aec72 new file mode 100644 index 0000000..963841c --- /dev/null +++ b/tests/fixtures/transactions/7d252165dbd5b147f47b0cf2ddba80179e8ff7f3f2fdfbbf85b505a4980aec72 @@ -0,0 +1 @@ +010000000001022be647fc8fad936f2cf1163ddb5e8b1ae922b49efa68c65743d11f042f277b2401000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff88c55c42535ec600b5a451e3229c6531e6143c4b396af94c869eadb6fdfea22001000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff0290d003000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e1487a0cb02000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e1487050047304402207a0cdde63eecc80583f4a175564fc127461c8dd00b054c588e48b5cf83184a33022066f21f66831ba67aab371fe4b856fd9a2ed5f0f401b8a66baa825cae81f218a601483045022100f9fbc43e89445c0920a48cb94282b618c9f0d3c0a76a16403257c244e99756dd02203a4bceb2ba8bffbb6fb067fb2b256cfa84a503a4679c0ba357ee20d83795e610014730440220171e8d4cd03938452835a9561ae809314e2656b20befc505c2972d190f470950022044c1b2e7ee9a807b6bb56f3d6941dd93ab1b849aa943b10499c91a1adddb3d0001f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae05004730440220255e4e7937d895a12b5e38517ebe71f3ad9ac307b89456171aab60db42c8bb5f02202b5d85127d55b3278f9c45d0e83393b52370ee26e6fa16cba4e4793a9c503876014730440220479ba2ffa538ba83719457d6312a1745fbb32ffc24bec81c4f538d6a60c0f02202200736c5575f508e2e098d649947de8170414d057cf2a54aa50d9f6e801e04e74b01483045022100c8c87560e998212b6769040649c293f99ec754060629534fd1471f0a76301ab60220421388eb683d3e777278b384cc7773e3cc14b786ece6bbd612468f354413e11a01f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/953a78a6773d83e0539d5e4c0d108d8a2af1321c2bc6295e7d4e4b00adbea08c b/tests/fixtures/transactions/953a78a6773d83e0539d5e4c0d108d8a2af1321c2bc6295e7d4e4b00adbea08c new file mode 100644 index 0000000..c73bd77 --- /dev/null +++ b/tests/fixtures/transactions/953a78a6773d83e0539d5e4c0d108d8a2af1321c2bc6295e7d4e4b00adbea08c @@ -0,0 +1 @@ +01000000000102176a58e9487d81696c333e66e047bb4e46890cfe10a2bf79b69f632e7404fc2a01000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff616ff91ea2f040f3dae58ede57385734e675f2db53983410bd49fce63c0e430b00000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff0240bc03000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e1487607403000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e14870500473044022032ec56d349744b281c3c694cf58a9ecb476ff05c83ed13a2a5af624ea5f9be0d02206fc37ea542ae879db8fb26695360f8ebf178eb9b692ed7abcf9f69869f952dd201483045022100a67e22c739bf46a5fa5dd7dae043ae560d79df7a5d16bdaaa0086ece734eb43002206e63c4dba111dbe3f4c8fe078f08fc04526df919e5a8f19dd55a61c03cce516601483045022100d3fa1737ae2a1b5155870d7130594b3f6490290e8730d0edd74446f92fc9d6ff02204246b67a63f80c0c2fb5784b89b907f6f9bf53b20d99866b9ddaae8478083b9901f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae0500483045022100ac6d083a2efadfc31cb792d0548bccbdb1ff374f6a4fe678873deb20782652e30220632972e8545712c4af6795fe4d1a556fb9f631dfa96f97a555f5a9f9e250116401483045022100c32eb53007ffb20dd1d554e118fa78aa63f72c02a5b8a70b454f6fcd5b7c10cc022061d8df1c9119d63fb3a6554e013590203e0e56a1b9dcbfd02fba023bd172327901473044022068fb7c95398c78f5e8b0f997a738e2e474cc80ad9cbd904d0bf725b6ac59d5920220099dad6fd594385246437f87552440a435cd2a210db694999de418b4b1da8f7501f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/9d5ce4c39fe62f7567737d0c50fa7a0561c4b298f93ab38d06a609a41a8d22c3 b/tests/fixtures/transactions/9d5ce4c39fe62f7567737d0c50fa7a0561c4b298f93ab38d06a609a41a8d22c3 new file mode 100644 index 0000000..7fdd023 --- /dev/null +++ b/tests/fixtures/transactions/9d5ce4c39fe62f7567737d0c50fa7a0561c4b298f93ab38d06a609a41a8d22c3 @@ -0,0 +1 @@ +01000000000102ac24c8bc1ba05294b09af44428964f43b42cb26236cc529ba466a2a93397323c01000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffffde25520454cb313de5e16347a7789c69fc93c67d72af3824a17b5f18e2f3a05600000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff0290d003000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148750b103000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e14870500483045022100f7bd3dd1e7eea46fad901bc612a7161ab31638e9f61facb3dee6d796a74217bf022063bef54ac5de8307a2b1a891fb593d9c4f36b8e8a84385f618a4e6ee1ae1793701473044022044d4c4f05b6746cbaa19164f4511f0973ff449df3bd98409cc60bad02eb8afd7022069ac8ec8f0b1c2bab1b7869d8e12c5f4aca3621752ac64f1db9dca3569d093ce01473044022065ae7854cb81fed5949eb0d6f6aec94636506485d7099b8aa61ff812bad6f4b40220586ceda0db8e1f62e7458d55134be8ea6c9b6ac3399be1c1feef868b557847ec01f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae050047304402203c7ec84768f5cfa6164c7b522f801e683cd16dcf33f060cd493e3b06683f39c202200af9a0fbfd7fc59ee670c2420325622469eaaa2e9de23d73f0ba6707537ea13e0147304402201fd40166bb60d7b980f5514ca9900887624c91fc4249cd66d3df885263c0e10102202338f106bb6c3b8252d46ffb309be6d9be490ba71d4ccff2ce5cd4788f2f2b8e01483045022100f09da21843e061d1f3f61ec4e447dca0003447c1f9ce58d5ae4ec8908135fcb4022058e1ec8cc52a7a749ba8d71db92aa37570350d641949d5cebed00dc74c627d4e01f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/c510fd107826af20ef7981bedf37f684b39a96d3787bc4ce376143d65e596415 b/tests/fixtures/transactions/c510fd107826af20ef7981bedf37f684b39a96d3787bc4ce376143d65e596415 new file mode 100644 index 0000000..6030517 --- /dev/null +++ b/tests/fixtures/transactions/c510fd107826af20ef7981bedf37f684b39a96d3787bc4ce376143d65e596415 @@ -0,0 +1 @@ +010000000001019782e01211566c65b2a16bc28188dc02fb71629256488c00db0deafcef2b4d4501000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff022838fc020000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148754cb03000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e14870500473044022061a30e677169a24c4ee9a4e1983dd3cad1334a31767f6fa92862a1a02f89e7de02201116a5afc165a755bbf769bb55ea84f697a8d3f2416f3188e61c5d1daa7ddb1301473044022025b77e6488074430fdc7f2f85a247105022c03866c334bdbd33f3501cb3f6197022041cbe700789e1fc7bd532f2492ea8b8bf636d8db70d28e902e1185cb562e954301483045022100ccd6de085a7abd18dfab417064ca7857857db7bea019d52a3bb0452f8de439af02203253d98462883ddd64cbdfb2e34fa74781c7736799e35af9d94f7e7932a783f401f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/cc7173bed4defad6a6304d76f54f67049875ce639f90a5c1d42dae3d8262c4b7 b/tests/fixtures/transactions/cc7173bed4defad6a6304d76f54f67049875ce639f90a5c1d42dae3d8262c4b7 new file mode 100644 index 0000000..476431e --- /dev/null +++ b/tests/fixtures/transactions/cc7173bed4defad6a6304d76f54f67049875ce639f90a5c1d42dae3d8262c4b7 @@ -0,0 +1 @@ +010000000001021564595ed6436137cec47b78d3969ab384f637dfbe8179ef20af267810fd10c501000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffffd8d32a8f2f10077ecc4182ad0da3eaf6acb1858d5da6e02116b8f7e7820a07d201000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff02149303000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148790d003000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e1487050047304402203efb415aadf2b079276beb9251166423ae0ad3554880edf21e4687c71a5d0970022054a06460f64f5fd39472072b02a1e3066d26b46315296c2f3605edee947a14790147304402204920e2670735a15adef40f9d4a35c6ab05478ac16773d6b2ad7e73f293b0ae3a022010f56a7d72325b5fea12038be0c0e3843e7d172caf2b37e756d55f2264abcccd014730440220405fc278f39ea0a0f540f4c16d66f40fe9ca729a222e16ddd2352f7e46d443f502205932c39c20d227f70b9774a43b2a2ea900746e6929b481aede54f99c72fa432301f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae05004830450221009e5e9ed84aca7e214f51395eb19ca9d74c893b75e3c051d028609d8973a7da9402203e5aff0040cbafb08aa9ad11a7f638547bf3e2643c63e7b1c29b5d323450b47f0147304402200977227a7da824912683cb9dca7f766aa6ff4a9c72362092164c8c37ce702f4702206df7b8024468b52ca7936a03fc1d7c79dd30254e8ee66f2ed6b2e2fd85d0c2010147304402203d71bb5c5b66edb5c5b42631ef1475274045733a54c4a6069275dc853ae1ffa102203f8f1fbd938baa8ffe5c7e0acfe5e3a7acd0333f0141f8cb9925c248ba6e308401f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/d979c1072803a92cff5e36c25a45b92dfe0fa086178815ba08d84791a6b5aa2b b/tests/fixtures/transactions/d979c1072803a92cff5e36c25a45b92dfe0fa086178815ba08d84791a6b5aa2b new file mode 100644 index 0000000..bd5d28f --- /dev/null +++ b/tests/fixtures/transactions/d979c1072803a92cff5e36c25a45b92dfe0fa086178815ba08d84791a6b5aa2b @@ -0,0 +1 @@ +0100000000010254f5506575fd9d5f5b36af3e2b0f3db4a3df0773b72384691cc54622c5e229e901000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffffb1d9e611f22747b9f8dd9397f9b62be99e91c796184059d160013d7fa8a9d44f00000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff02c4e802000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148740bc03000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148705004730440220125fab672e30401a96e98b812cf9a7e0715cddef56be70112204b4d432c87a23022024515453fad65a3c669780815ba633137e652fe167ec37af567ac403516ee8220147304402204d879718824cebb317e2a28afd81f02310c478e1ad1cafc67dd66a48d1e27bef0220710cf9913560019bd4b9b83edf073b4f98212176cb17f8e857416b968ef7d5d301483045022100cd0b5d35d5fb6722589cbf59964936be0b7315be7e41771a05c9a6198e4d91b002201842764bdeb4a987a5dd22bcb68557e65fe19898744e784e8e7fe78406ce8fd301f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae05004830450221008e58a91d8d1eba1b8e5e42a53f315358647c0c8388de262237532049576686c102206864749a9c4b9bc10c3c0cdafe1107ecdefef8b1fda5297d1dd6a36ab03646030147304402201a4f944a3d8f0efc732aa852d5cca29012a0e61fa3566fc91c1cf0c463a8369902206658f4a7b0163bb5c5bb74353347ab65c0db313f1cb72893e6725237cf6729bb01483045022100b0c65ac43c2765d93a364b87c9bdb9216e121c8eefc6c84a64fd4b8615b9a410022030161fe0588643d16313b511c17bd1db31b1d3274d3f2d330555e269218abb1401f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/tests/fixtures/transactions/e9170ceec57cefd0a9eba18d92720f152c162eb0353988b63e1412c4852399f2 b/tests/fixtures/transactions/e9170ceec57cefd0a9eba18d92720f152c162eb0353988b63e1412c4852399f2 new file mode 100644 index 0000000..411f7d4 --- /dev/null +++ b/tests/fixtures/transactions/e9170ceec57cefd0a9eba18d92720f152c162eb0353988b63e1412c4852399f2 @@ -0,0 +1 @@ +0100000000010266725688ab94e8d7bce39c704b9f93c068a55bb96172a53d4380287d7b02eb7701000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffffe6959f0a041e0f6d1175662460c41231aed4f4da2e9169c014460837d5f9b07300000000232200207410e2a9cc7b5742ed1a00ebf02a386e778daade1da0b415baaeeaf0402f4f0dfdffffff02706903000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148740bc03000000000017a914108d4807e968d5162ec9ce2ca08ba8ee04025e148705004830450221008b9005eda42a15cf1a1b11fac38dd5ed27c2f6ef09c9e9d09b754276580381fa0220183417cfceb7cda97aa7649c50163a5dae90a73a23874cfbbe631504cfac76aa01473044022053571c0912ed7791ede16895129699d95f7006ae986df6b17c13a1070f934b3502207378fab1149e93c69ab10e16ba740173e214bfb88ce10698f504bc5c340444650147304402207d2923b3696838a4710642ca9bacde7ad82acf35196f230c0cc151ead2b3fc720220388cd1c25056dfdb5b0457dc2a78a50b648cf2a0cdeb0fa5ade62ca6a7d236b001f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae05004730440220563eaac97bf63fb2ab9421d77dc5a54fc89cf532673940f3ee5664d529f9d63302207578816f9b242f0d53c223d1d5f0db28dde108906afe04b5903aec849f2dd2ec01473044022011596976c92831906c8a00d37c79e3717ccae2e89e1ace092489307da06598d102203f1d1dceda9152ebd41ab27bcfe9a901f19caabd0d2dd4e87dbcaa23aacbe90e01483045022100caad824bfa92ff7fa3a31431b848c9f2ba3bb0df62c220f0cbe244ae6110b6c3022051e236d2732191972f41c1e5168ff92a00321653fae6cf528e03193d0c20a11301f15321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae00000000 \ No newline at end of file diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..4672a90 --- /dev/null +++ b/web/app.js @@ -0,0 +1,157 @@ +import init, { verifyProofOfReserves } from './bdk_reserves_web.js' + +const form = document.getElementById('form') +const submit = document.getElementById('submit') +const example = document.getElementById('example') +const result = document.getElementById('result') + +const SATS_PER_BTC = 100_000_000n + +// Set when the form holds the bundled example, so a failure can say that it is +// expected. Typing in any field clears it; assigning .value does not fire input. +let loadedExample = null +form.addEventListener('input', () => { + loadedExample = null +}) + +function formatBtc(sats) { + const value = BigInt(sats) + const whole = value / SATS_PER_BTC + const fraction = (value % SATS_PER_BTC).toString().padStart(8, '0') + return `${whole.toLocaleString('en-US')}.${fraction}` +} + +function show(kind, html) { + result.className = kind + result.innerHTML = html +} + +function escape(text) { + const node = document.createElement('div') + node.textContent = text + return node.innerHTML +} + +// bdk-reserves reports its verdict as a Rust enum variant, which is precise but +// not much help to someone holding a proof that just failed. Say what each one +// actually means. The raw message is still shown underneath. +function explain(message) { + const nonSpendable = message.match(/NonSpendableInput\((\d+)\)/) + if (nonSpendable) { + return `The proof claims input ${nonSpendable[1]} (counting the challenge input as 0), but the + Esplora server does not list that output as unspent. Either those coins have been spent + since the proof was made, or they are not buried deep enough for the confirmation + setting above.` + } + if (message.includes('ChallengeInputMismatch')) { + return 'The proof was made for a different message. It has to match exactly, including punctuation and capitalisation.' + } + const notSigned = message.match(/NotSignedInput\((\d+)\)/) + if (notSigned) { + return `Input ${notSigned[1]} carries no signature, so the proof does not demonstrate control of those coins.` + } + if (message.includes('SignatureValidation')) { + return 'A signature in the proof is not valid for the output it spends.' + } + if (message.includes('InAndOutValueNotEqual')) { + return 'The amounts going in and out of the proof do not match, so it is not a well formed proof of reserves.' + } + if (message.includes('UnsupportedSighashType')) { + return 'An input is signed with a sighash type other than SIGHASH_ALL, which a proof of reserves cannot use.' + } + if (message.includes('WrongNumberOfInputs') || message.includes('WrongNumberOfOutputs')) { + return 'The PSBT is not shaped like a BIP-127 proof: it should have one challenge input, one input per UTXO, and a single unspendable output.' + } + if (message.includes('InvalidOutput')) { + return 'The proof does not pay to the unspendable output BIP-127 requires.' + } + return null +} + +// Addresses are accepted one per line or comma separated, whichever is handier +// to paste. +function parseAddresses(raw) { + return raw + .split(/[\s,]+/) + .map((address) => address.trim()) + .filter(Boolean) +} + +// The example proof is fetched rather than inlined, because the PSBT alone is +// 28kB of base64 and most visitors never ask for it. +example.addEventListener('click', async () => { + example.disabled = true + try { + const meta = await fetch('example/proof.json').then((response) => response.json()) + const psbt = await fetch(`example/${meta.proof_psbt}`).then((response) => response.text()) + + document.getElementById('addresses').value = meta.addresses.join('\n') + document.getElementById('message').value = meta.message + document.getElementById('psbt').value = psbt.trim() + document.getElementById('confirmations').value = meta.confirmations + + loadedExample = meta + show('info', `

Example loaded

${escape(meta.note)}

`) + } catch (error) { + show('err', `

Could not load the example

${escape(String(error))}

`) + } finally { + example.disabled = false + } +}) + +form.addEventListener('submit', async (event) => { + event.preventDefault() + submit.disabled = true + submit.textContent = 'Verifying…' + show('', '') + + const request = { + addresses: parseAddresses(document.getElementById('addresses').value), + message: document.getElementById('message').value, + proof_psbt: document.getElementById('psbt').value, + confirmations: Number(document.getElementById('confirmations').value), + esplora_url: document.getElementById('esplora').value.trim() || null, + } + + try { + const proof = await verifyProofOfReserves(request) + show( + 'ok', + `

Proof verified

+
${formatBtc(proof.spendable)} BTC
+

+ ${proof.spendable.toLocaleString('en-US')} sats across ${proof.utxos} + UTXO${proof.utxos === 1 ? '' : 's'} on ${escape(proof.network)}, + confirmed as of block ${proof.tip_height.toLocaleString('en-US')}.
+ Looked up via ${escape(proof.esplora_url)} +

`, + ) + } catch (error) { + const message = String(error) + const reason = explain(message) + const expected = + loadedExample && message.includes('NonSpendableInput') + ? `

This is the expected result for the bundled example. ${escape(loadedExample.note)}

` + : '' + show( + 'err', + `

Not verified

+ ${reason ? `

${reason}

` : ''} + ${expected} +

${escape(message)}

`, + ) + } finally { + submit.disabled = false + submit.textContent = 'Verify' + } +}) + +init() + .then(() => { + submit.disabled = false + submit.textContent = 'Verify' + }) + .catch((error) => { + show('err', `

Could not start

${escape(String(error))}

`) + submit.textContent = 'Unavailable' + }) diff --git a/web/example/proof.json b/web/example/proof.json new file mode 100644 index 0000000..e2b7b19 --- /dev/null +++ b/web/example/proof.json @@ -0,0 +1,9 @@ +{ + "addresses": [ + "2Mtkk3kjyN8hgdGXPuJCNnwS3BBY4K2frhY" + ], + "message": "Stored in SEBA Bank AG cold storage", + "proof_psbt": "proof.psbt.base64", + "confirmations": 3, + "note": "A real testnet proof of 0.5386558 BTC over 18 UTXOs. Those UTXOs have since been spent, so verifying it against a live server reports NonSpendableInput. That is the correct answer: a proof only holds while the coins behind it are still there." +} diff --git a/web/example/proof.psbt.base64 b/web/example/proof.psbt.base64 new file mode 100644 index 0000000..dec198c --- /dev/null +++ b/web/example/proof.psbt.base64 @@ -0,0 +1 @@ +cHNidP8BAP03AwEAAAATfUqjtTyZAxfGOsqFi93k3ToGtPZ0E/BZWFlBgAFlt1kAAAAAAP////8VZFle1kNhN87Ee3jTlpqzhPY3376Bee8gryZ4EP0QxQAAAAAA/////xdqWOlIfYFpbDM+ZuBHu05GiQz+EKK/ebafYy50BPwqAAAAAAD/////K6q1ppFH2Ai6FYgXhqAP/i25RVrCNl7/LKkDKAfBedkAAAAAAP////8rqrWmkUfYCLoViBeGoA/+LblFWsI2Xv8sqQMoB8F52QEAAAAA/////yvmR/yPrZNvLPEWPdteixrpIrSe+mjGV0PRHwQvJ3skAAAAAAD/////cuwKmKQFtYW/+/3y8/ePnheAut3yDHv0R7HV22UhJX0AAAAAAP////9y7AqYpAW1hb/7/fLz94+eF4C63fIMe/RHsdXbZSElfQEAAAAA/////4ygvq0AS059XinGKxwy8SqKjRANTF6dU+CDPXemeDqVAAAAAAD/////jKC+rQBLTn1eKcYrHDLxKoqNEA1MXp1T4IM9d6Z4OpUBAAAAAP////+3xGKCPa4t1MGlkJ9jznWYBGdP9XZNMKbW+t7UvnNxzAAAAAAA/////7fEYoI9ri3UwaWQn2POdZgEZ0/1dk0wptb63tS+c3HMAQAAAAD/////wyKNGqQJpgaNszr5mLLEYQV6+lAMfXNndS/mn8PkXJ0AAAAAAP/////DIo0apAmmBo2zOvmYssRhBXr6UAx9c2d1L+afw+RcnQEAAAAA/////9Fninwz/x77J2ghJX0wcVNLRI3f3wMIlh5kePz8l2ZuAAAAAAD/////1AUOKakFoN1BqrDomHASI0VFsLtskXVQpPljoDU8zWsBAAAAAP/////swob+WCNq5562PWB+Z5JOFFogd/20GAr4Vyra6oOIAAAAAAAA/////+zChv5YI2rnnrY9YH5nkk4UWiB3/bQYCvhXKtrqg4gAAQAAAAD/////8pkjhcQSFD62iDk1sC4WLBUPcpKNoeup0O98xe4MF+kAAAAAAP////8BbOw1AwAAAAAZdqkUn3/QltN+0sDj9/DPySS+70/862iIrAAAAAAAAQEKAAAAAAAAAAABUQEHAAABASAoOPwCAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FSDBFAiEA6crnwxlLYnlcWc2LovFA7qbw017cI//bmND/tKSNuMkCIDMCDYT7WXeJ5BRJGZuA+MRNs6sWdxo2Yo47bkUPQCS5ASICA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRySDBFAiEAreZ3cbl2oT7kEw7IDoU7ZF23rij0KFtuV4RqvkuXDuoCICueWRN9+sizOalX9N6tIr9hKe+W2Ib14K1QrjoGKhYVASICA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTSDBFAiEAnBtH9h2MP0ket2WG17u+yY3i+dS+Udqejcbi50Y+7zICIAn38DAz3z/bPIr9gJnxBip8d5GwRxWe/zSsYrzDcM5YAQEEIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BBfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAQcjIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BCP3PAQUASDBFAiEA6crnwxlLYnlcWc2LovFA7qbw017cI//bmND/tKSNuMkCIDMCDYT7WXeJ5BRJGZuA+MRNs6sWdxo2Yo47bkUPQCS5AUgwRQIhAK3md3G5dqE+5BMOyA6FO2Rdt64o9ChbbleEar5Llw7qAiArnlkTffrIszmpV/TerSK/YSnvltiG9eCtUK46BioWFQFIMEUCIQCcG0f2HYw/SR63ZYbXu77JjeL51L5R2p6NxuLnRj7vMgIgCffwMDPfP9s8iv2AmfEGKnx3kbBHFZ7/NKxivMNwzlgB8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64AAQEgkNADAAAAAAAXqRQQjUgH6WjVFi7Jziygi6juBAJeFIciAgMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhUcwRAIgIPAaAfgPulkyQ5L6f2KTr7bIEWfBTBowsEyi9Aosr0ECIAsNTyysm/4CHhW4fN4dGC0JCUUedI0Z+0jldWcmiopoASICA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyRzBEAiBO/Hb8owJsvAwLlLhITvCDyb0F4AcJ49xlIdiQcM0ETQIgWHvNFlXDhYjeCl3H9u0Jc/tEAhbTxTgFDR07DdaIcK0BIgID9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNHMEQCIBslyLRBXqm+kwjMszeUNWFBX8iZpeihmlo1s47BbMT/AiAYafOyRO+LmBm4x+EWMZ3VDzauxhung7FJAm/598b6mAEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zAEFAEcwRAIgIPAaAfgPulkyQ5L6f2KTr7bIEWfBTBowsEyi9Aosr0ECIAsNTyysm/4CHhW4fN4dGC0JCUUedI0Z+0jldWcmiopoAUcwRAIgTvx2/KMCbLwMC5S4SE7wg8m9BeAHCePcZSHYkHDNBE0CIFh7zRZVw4WI3gpdx/btCXP7RAIW08U4BQ0dOw3WiHCtAUcwRAIgGyXItEFeqb6TCMyzN5Q1YUFfyJml6KGaWjWzjsFsxP8CIBhp87JE74uYGbjH4RYxndUPNq7GG6eDsUkCb/n3xvqYAfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAAEBIMToAgAAAAAAF6kUEI1IB+lo1RYuyc4soIuo7gQCXhSHIgIDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroVIMEUCIQC0teI6jSpNvTYMnaPvHBLHz8xeV78YSKHP0wDLTeIFggIgKJwbaMl8W0lphJppl+GpIda/WuptemyTsvvRxfDZh8IBIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJHMEQCIAuqur8TVlHrIYOWS8H1DM0ujqJOOPRrTzHHNY/PxsYEAiAq8VxXwyEEb+6DtbhYVffNGPsLI8KursWz162rnUw7XAEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0YwQwIfOF7R8ET9GpC3LilmUZ/oTO3kCtEO33jfcRxTZMaf0gIgQ3PLFN/ia0aSa3ZjSGoXT6at1OmFDaw0JVcdUh5KQskBAQQiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEF8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64BByMiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEI/cwBBQBIMEUCIQC0teI6jSpNvTYMnaPvHBLHz8xeV78YSKHP0wDLTeIFggIgKJwbaMl8W0lphJppl+GpIda/WuptemyTsvvRxfDZh8IBRzBEAiALqrq/E1ZR6yGDlkvB9QzNLo6iTjj0a08xxzWPz8bGBAIgKvFcV8MhBG/ug7W4WFX3zRj7CyPCrq7Fs9etq51MO1wBRjBDAh84XtHwRP0akLcuKWZRn+hM7eQK0Q7feN9xHFNkxp/SAiBDc8sU3+JrRpJrdmNIahdPpq3U6YUNrDQlVx1SHkpCyQHxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgABASBAvAMAAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FSDBFAiEA1/g2rzRk8SH4joG6KgolR3Duzs6MRsoqDHsYQFxpOeUCIFJNPgKVhztuek3nslD5goODjy9uH7zyxeCH1IpnVng+ASICA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyRzBEAiB62Ajtue1nb0g5UPhCD/0XTeeMXOhkXIVzV97pSYwJQgIgY1jbyOjos8QBtSmSUsMinsYwUDusy5ipu20YLh4iPJQBIgID9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNIMEUCIQDL2gnT2r42FEhLgAiZnR8jsPTIeoJXFwhCFRmtZNR6qgIgOQqJSY75A5yNYU7iL46rrAA2OXN9VKORVqywBKEAQCABAQQiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEF8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64BByMiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEI/c4BBQBIMEUCIQDX+DavNGTxIfiOgboqCiVHcO7OzoxGyioMexhAXGk55QIgUk0+ApWHO256TeeyUPmCg4OPL24fvPLF4IfUimdWeD4BRzBEAiB62Ajtue1nb0g5UPhCD/0XTeeMXOhkXIVzV97pSYwJQgIgY1jbyOjos8QBtSmSUsMinsYwUDusy5ipu20YLh4iPJQBSDBFAiEAy9oJ09q+NhRIS4AImZ0fI7D0yHqCVxcIQhUZrWTUeqoCIDkKiUmO+QOcjWFO4i+Oq6wANjlzfVSjkVassAShAEAgAfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAAEBIJDQAwAAAAAAF6kUEI1IB+lo1RYuyc4soIuo7gQCXhSHIgIDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroVHMEQCIG8RbiU4pfej6nYCvbRERTrOV7THtJ/xiFL83iKmn0STAiAgZE3tv89cnDkXzkUF/NWLu7jgx2aIOIw+oux59Ad89gEiAgN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkckgwRQIhAN3hBE1+lZG9CspFe2Vi99jCfdxp7uT9wahGSKetI7DyAiACfY4axH2e8AC9HxxlUdEv3tF966p1AkRyXFVnFvKOiQEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0gwRQIhAJxmYXVPXR8U+T7JAoacKaZ3LxKmGDOp6ZejLp1kBC2DAiB+9szpX3hTOhHXKyiJqCh/sOqI4JLN/lBZ/0+7Ib0keAEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zgEFAEcwRAIgbxFuJTil96PqdgK9tERFOs5XtMe0n/GIUvzeIqafRJMCICBkTe2/z1ycORfORQX81Yu7uODHZog4jD6i7Hn0B3z2AUgwRQIhAN3hBE1+lZG9CspFe2Vi99jCfdxp7uT9wahGSKetI7DyAiACfY4axH2e8AC9HxxlUdEv3tF966p1AkRyXFVnFvKOiQFIMEUCIQCcZmF1T10fFPk+yQKGnCmmdy8SphgzqemXoy6dZAQtgwIgfvbM6V94UzoR1ysoiagof7DqiOCSzf5QWf9PuyG9JHgB8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64AAQEgkNADAAAAAAAXqRQQjUgH6WjVFi7Jziygi6juBAJeFIciAgMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhUgwRQIhAJe1Byz1N0Z9WujF/vKFS9aHSpDQmC7lx2nvWACr5RCHAiAvVK+MUJuIIAh5+W5tZI/DMoN2V72My/8Mb/Qf29jsUgEiAgN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkckcwRAIgQvtUBnniirlsWgZ28sS44likUKFj+BjKIGxU7x2UFnACIDj3WbTWwLNVjZmCjKlQLF9IxuUcRHFkn+psFxjgmmhLASICA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTRzBEAiAdNVtbZok1kacUYjwCD4G0iAIZoVIHYwcVhy+bcsKlEQIgVfmeZ9ATULCT21SF7AGuRsvPFFQNvZxOHj8nYCrFr3IBAQQiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEF8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64BByMiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEI/c0BBQBIMEUCIQCXtQcs9TdGfVroxf7yhUvWh0qQ0Jgu5cdp71gAq+UQhwIgL1SvjFCbiCAIeflubWSPwzKDdle9jMv/DG/0H9vY7FIBRzBEAiBC+1QGeeKKuWxaBnbyxLjiWKRQoWP4GMogbFTvHZQWcAIgOPdZtNbAs1WNmYKMqVAsX0jG5RxEcWSf6mwXGOCaaEsBRzBEAiAdNVtbZok1kacUYjwCD4G0iAIZoVIHYwcVhy+bcsKlEQIgVfmeZ9ATULCT21SF7AGuRsvPFFQNvZxOHj8nYCrFr3IB8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64AAQEgoMsCAAAAAAAXqRQQjUgH6WjVFi7Jziygi6juBAJeFIciAgMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhUgwRQIhAIaWIg/RLaQ2Kv2PJZBwrVsK6QkGO5oc6Gax5pMUJu1HAiAGLpU1ShiqbbGpnC1t6K0zYWMPfm5XuHKNfI/Z5XwJrwEiAgN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkckgwRQIhALkJ3WI0WfmEDEWB8yN8J1jqyY92BoFGyJOmB8nAbZNeAiAgzrzyb2wLaVyl4LXFHE40GTa6HkmopRDN+35zJZb2yQEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0gwRQIhANKRAxXD6w6U66SVPj+6JtU2u10UttnkCIoQZKBadntDAiAvcgibqGLyogPYkZgtI72qs2coYX3zrOfLOdPDOtaYhgEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zwEFAEgwRQIhAIaWIg/RLaQ2Kv2PJZBwrVsK6QkGO5oc6Gax5pMUJu1HAiAGLpU1ShiqbbGpnC1t6K0zYWMPfm5XuHKNfI/Z5XwJrwFIMEUCIQC5Cd1iNFn5hAxFgfMjfCdY6smPdgaBRsiTpgfJwG2TXgIgIM688m9sC2lcpeC1xRxONBk2uh5JqKUQzft+cyWW9skBSDBFAiEA0pEDFcPrDpTrpJU+P7om1Ta7XRS22eQIihBkoFp2e0MCIC9yCJuoYvKiA9iRmC0jvaqzZyhhffOs58s508M61piGAfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAAEBIEC8AwAAAAAAF6kUEI1IB+lo1RYuyc4soIuo7gQCXhSHIgIDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroVHMEQCIDcrSuHCIy8dYDwYX2fk04o7gNLgAKGPIL9TJMfa1HwTAiAFTr+kHxCeNPAad8ueul5ZqEU0aasIHitJQMmMgepoDwEiAgN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkckgwRQIhANyomNej5S0KfovKPU29hzDyylO/E1QGJXlrvV6QLj/NAiAprLPC3aNM5jQ6gxF7Uv7kgf+x9Tb4/OEIMvDdEal/wgEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0cwRAIgDBhvIR9ZZzL5bRf6PMMbpi1v7/13gK/CJScbtapq3egCIFW2hwPcFDNGRfI25E8qxgSKaeIJmF+3nKEN5aX+ct/CAQEEIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BBfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAQcjIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BCP3NAQUARzBEAiA3K0rhwiMvHWA8GF9n5NOKO4DS4AChjyC/UyTH2tR8EwIgBU6/pB8QnjTwGnfLnrpeWahFNGmrCB4rSUDJjIHqaA8BSDBFAiEA3KiY16PlLQp+i8o9Tb2HMPLKU78TVAYleWu9XpAuP80CICmss8Ldo0zmNDqDEXtS/uSB/7H1Nvj84Qgy8N0RqX/CAUcwRAIgDBhvIR9ZZzL5bRf6PMMbpi1v7/13gK/CJScbtapq3egCIFW2hwPcFDNGRfI25E8qxgSKaeIJmF+3nKEN5aX+ct/CAfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAAEBIGB0AwAAAAAAF6kUEI1IB+lo1RYuyc4soIuo7gQCXhSHIgIDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroVIMEUCIQDiOpeNLn9TUH52eaL716+dvJG3izzKgeNikj6rG0UWZgIgJJPWxBKq6wWUoOoLfNsdvXqehOqMzAPdvWcRVAWE3mgBIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJIMEUCIQCbQIiX6cMvg8tBX+NGPcUlrlNOD2NcOIYem7f0JTn9eAIgBeTpWQU5o3+Gj0pNdcDMZCOfIDRVxqUj4N8wdNsxXAcBIgID9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNIMEUCIQCJSwnjDM8L3cLDOvuPEZBu/ZNvy8nccMZgquCCBsZ/RQIgdyRS2fD9JzLsfj0cY5ISJlLs63R4uEd4ZHv25a/2ysgBAQQiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEF8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64BByMiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEI/c8BBQBIMEUCIQDiOpeNLn9TUH52eaL716+dvJG3izzKgeNikj6rG0UWZgIgJJPWxBKq6wWUoOoLfNsdvXqehOqMzAPdvWcRVAWE3mgBSDBFAiEAm0CIl+nDL4PLQV/jRj3FJa5TTg9jXDiGHpu39CU5/XgCIAXk6VkFOaN/ho9KTXXAzGQjnyA0VcalI+DfMHTbMVwHAUgwRQIhAIlLCeMMzwvdwsM6+48RkG79k2/LydxwxmCq4IIGxn9FAiB3JFLZ8P0nMux+PRxjkhImUuzrdHi4R3hke/blr/bKyAHxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgABASAUkwMAAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FSDBFAiEAgOQshynIa1z5uOeZe1LzWwJJnpfNw0ioRQU8LNFuHzsCIC0fRCyCT/Lbv7aOFAPaV2MPE3fcSRbHoatLebaur3dHASICA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyRzBEAiAkRkDvUp/7DYDkjA2PqbL6hYsXaQfhjN34JQxofNQ8jQIgZlndSNbEE6ftp1M/+DOmi8G/eBO+iux5skc2FDFR/qkBIgID9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNIMEUCIQDvcrYYdDvLUpxX9E8CXV64vL+71+Ae1bXGZsUrERWeJAIgTUwCgbqNXQBv8rfs1plIbW0WgRuXRfZykTAfivyNZDABAQQiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEF8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64BByMiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEI/c4BBQBIMEUCIQCA5CyHKchrXPm455l7UvNbAkmel83DSKhFBTws0W4fOwIgLR9ELIJP8tu/to4UA9pXYw8Td9xJFsehq0t5tq6vd0cBRzBEAiAkRkDvUp/7DYDkjA2PqbL6hYsXaQfhjN34JQxofNQ8jQIgZlndSNbEE6ftp1M/+DOmi8G/eBO+iux5skc2FDFR/qkBSDBFAiEA73K2GHQ7y1KcV/RPAl1euLy/u9fgHtW1xmbFKxEVniQCIE1MAoG6jV0Ab/K37NaZSG1tFoEbl0X2cpEwH4r8jWQwAfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAAEBIJDQAwAAAAAAF6kUEI1IB+lo1RYuyc4soIuo7gQCXhSHIgIDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroVIMEUCIQC2uoCNKThiMkY4hS0N/RjgjTL9xHyXfpUu8YRhG8IpsAIgbsPsv6IVfIOfkOjeLCOZ0M3HaY4y2VGjtlimyYKxajwBIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJHMEQCID4kiDHLNloY2scrbYxkbLYl0tztci2c8z6OCcd4tANmAiB063HT9xQXn3hxyCbkSQbspPuggC6/o/rCWj3pyZgtqQEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0gwRQIhANabxBh+izQPr11tLskTDYo0TPBwW/FCnUPd4gYzdkZXAiAKfVxwkrXQEPWKfUadqfkuYTO9Ts85LKB4g+3ul+EcQQEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zgEFAEgwRQIhALa6gI0pOGIyRjiFLQ39GOCNMv3EfJd+lS7xhGEbwimwAiBuw+y/ohV8g5+Q6N4sI5nQzcdpjjLZUaO2WKbJgrFqPAFHMEQCID4kiDHLNloY2scrbYxkbLYl0tztci2c8z6OCcd4tANmAiB063HT9xQXn3hxyCbkSQbspPuggC6/o/rCWj3pyZgtqQFIMEUCIQDWm8QYfos0D69dbS7JEw2KNEzwcFvxQp1D3eIGM3ZGVwIgCn1ccJK10BD1in1Gnan5LmEzvU7POSygeIPt7pfhHEEB8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64AAQEgkNADAAAAAAAXqRQQjUgH6WjVFi7Jziygi6juBAJeFIciAgMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhUgwRQIhAJi8clrjwM1svGNRYYAjbDVdW1Dy/qcwbzRdkK22ZxmJAiAWYiFtqswmynT8tMxXCkCUXiTwO5S47DzB+c95bEcQRwEiAgN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkckgwRQIhAOYEdTY4CF6uEbZUq+0jHn2wWrRS+hSE9Pw/owayR76qAiBToNj2JBrMhiZmEDC4pom+5uq0lLkA1i3sU0Q/sGeBZgEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0gwRQIhALTX4VG5eJlIS4uNNWcBHWDuEDmHuJwdeLQNY3O0KaaYAiBpVSpEgvu8pcWo0Hedv9D9qZLnFGCx9ITt0462qLbNhAEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zwEFAEgwRQIhAJi8clrjwM1svGNRYYAjbDVdW1Dy/qcwbzRdkK22ZxmJAiAWYiFtqswmynT8tMxXCkCUXiTwO5S47DzB+c95bEcQRwFIMEUCIQDmBHU2OAherhG2VKvtIx59sFq0UvoUhPT8P6MGske+qgIgU6DY9iQazIYmZhAwuKaJvubqtJS5ANYt7FNEP7BngWYBSDBFAiEAtNfhUbl4mUhLi401ZwEdYO4QOYe4nB14tA1jc7QpppgCIGlVKkSC+7ylxajQd52/0P2pkucUYLH0hO3Tjraots2EAfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAAEBIFCxAwAAAAAAF6kUEI1IB+lo1RYuyc4soIuo7gQCXhSHIgIDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroVIMEUCIQCIM5+AE0LNL0dGLIWwwWL/sGLD9w4uqZBPD5wytiXE5QIgC7bB/hWaroji6p9U7dKeSwoXSlTpLJ6eTLl/ju1N/zYBIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJHMEQCIEiYTF/w3BSS0raWSeD5sZ3+xAVMq2wi3lhthIPrNYvEAiAfIvJGodRLNO//Rtdo8DFkvtx7Ea/lzWADz8ylHwDrywEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0gwRQIhANpOLX/6I5W/tzbXnGFOC1aIjTtsfT35FxzL6jxD0pKvAiA9vXRG9jderWI4cLIi0Q0rCmknKxY+Fm+bULY00JEZAgEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zgEFAEgwRQIhAIgzn4ATQs0vR0YshbDBYv+wYsP3Di6pkE8PnDK2JcTlAiALtsH+FZquiOLqn1Tt0p5LChdKVOksnp5MuX+O7U3/NgFHMEQCIEiYTF/w3BSS0raWSeD5sZ3+xAVMq2wi3lhthIPrNYvEAiAfIvJGodRLNO//Rtdo8DFkvtx7Ea/lzWADz8ylHwDrywFIMEUCIQDaTi1/+iOVv7c215xhTgtWiI07bH09+Rccy+o8Q9KSrwIgPb10RvY3Xq1iOHCyItENKwppJysWPhZvm1C2NNCRGQIB8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64AAQEgBOIAAAAAAAAXqRQQjUgH6WjVFi7Jziygi6juBAJeFIciAgMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhUcwRAIgZhRZTYdYBLBTgCCXf6UFkn31RHY7ed51EEfODPTP3FgCIBTu3pHyCvvQg2Z8ooA9qs4HQyFDy2wVWER6sRW9qEsTASICA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRySDBFAiEAp20ai85KnRTfxfhULKMBZBO65gJ6lCyoUw01O3BbO3gCIHs5mPC4WIxiHmbHCDrIClZ6hfA5E741zGRJNsTl4i2aASICA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTRzBEAiB0HiwaMfMG+/AtVIjNS6AJR2TcDtLEpLNlC7idavov+QIgRd3RJsUWYv9v+RSf3D6SzetUH5s6ua9RiKNVi4BQ6+cBAQQiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEF8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64BByMiACB0EOKpzHtXQu0aAOvwKjhud42q3h2gtBW6rurwQC9PDQEI/c0BBQBHMEQCIGYUWU2HWASwU4Agl3+lBZJ99UR2O3nedRBHzgz0z9xYAiAU7t6R8gr70INmfKKAParOB0MhQ8tsFVhEerEVvahLEwFIMEUCIQCnbRqLzkqdFN/F+FQsowFkE7rmAnqULKhTDTU7cFs7eAIgezmY8LhYjGIeZscIOsgKVnqF8DkTvjXMZEk2xOXiLZoBRzBEAiB0HiwaMfMG+/AtVIjNS6AJR2TcDtLEpLNlC7idavov+QIgRd3RJsUWYv9v+RSf3D6SzetUH5s6ua9RiKNVi4BQ6+cB8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64AAQEgQA0DAAAAAAAXqRQQjUgH6WjVFi7Jziygi6juBAJeFIciAgMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhUgwRQIhALcITbBJx25ndqVAny1w6130pNTZTe/v6eWt87SOY3isAiAOp4eItSRav49fOE3+HsF8eJlyImn1MLEJiBxdsyhkhwEiAgN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkckgwRQIhAJ4PzjzZbK3tAb1V7af8jEPd3PLA+7BaeyyaBfofoNlPAiBFkygCOx0q49gnpuwe61MupyY/Fcp4ZsAZWzp42qSoNQEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0cwRAIgH8CbB/1fbi7RGIVFpJP91vbTIkdOqJ01WwP/tAHRX1MCIFj14hUjOIFgXLqD1Ztvftgxz4Oa8iv+4YmRtrXHjC6JAQEEIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BBfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAQcjIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BCP3OAQUASDBFAiEAtwhNsEnHbmd2pUCfLXDrXfSk1NlN7+/p5a3ztI5jeKwCIA6nh4i1JFq/j184Tf4ewXx4mXIiafUwsQmIHF2zKGSHAUgwRQIhAJ4PzjzZbK3tAb1V7af8jEPd3PLA+7BaeyyaBfofoNlPAiBFkygCOx0q49gnpuwe61MupyY/Fcp4ZsAZWzp42qSoNQFHMEQCIB/Amwf9X24u0RiFRaST/db20yJHTqidNVsD/7QB0V9TAiBY9eIVIziBYFy6g9Wbb37YMc+DmvIr/uGJkba1x4wuiQHxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgABASCIkgMAAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FRzBEAiBDugO7p7KtJGKocdosR80FvkGdE7LFEKLR3nAjqgoR5gIgVw9P3kUPCFF9d6eBvCjn5Y/YJdgVNNW6uO6CBgdH+QsBIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJHMEQCIFbi8jDcDE1sj58pcguIooGJXDhvrojvbG9CbozQPCjjAiAMHojZPBJP9LzKez7pgY+rANoKRgmxXaMjlu8kl9imHwEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0cwRAIgD3cndVkTdhnYwEhwYaBVWJP2E9jq1+ZQjOxNx+luXPMCICrD7DdQc314Ap9EAN+SO4jCeT9mHrVA+skEUoPCAxIdAQEEIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BBfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAQcjIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BCP3MAQUARzBEAiBDugO7p7KtJGKocdosR80FvkGdE7LFEKLR3nAjqgoR5gIgVw9P3kUPCFF9d6eBvCjn5Y/YJdgVNNW6uO6CBgdH+QsBRzBEAiBW4vIw3AxNbI+fKXILiKKBiVw4b66I72xvQm6M0Dwo4wIgDB6I2TwST/S8yns+6YGPqwDaCkYJsV2jI5bvJJfYph8BRzBEAiAPdyd1WRN2GdjASHBhoFVYk/YT2OrX5lCM7E3H6W5c8wIgKsPsN1BzfXgCn0QA35I7iMJ5P2YetUD6yQRSg8IDEh0B8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64AAQEgkNADAAAAAAAXqRQQjUgH6WjVFi7Jziygi6juBAJeFIciAgMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhUcwRAIgRYxL9S7/J5BX3SVNPySRxiXBrWTAihp3T4XxdNYz6D8CIG/e6bLrZqVUdQOtAbA41/es6Vy1hPIN6VAzFs9M5BVDASICA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRySDBFAiEAqBvDHFkWEXJfdqanzQJ25fUKXvjWUo/wa0otJAkBD1YCIGZhS3xBgLX/pHbmYg12ENLqGQIzIJrPPID3JFdTjq0VASICA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTSDBFAiEAslYryhKy5Njn1GNJt02Zugj62aOym3AsaRZiKj8aoD8CIA5KzBNNzfhCq6YMj+odwksJC3ctNT1eF5c4ijcxK5LGAQEEIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BBfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAQcjIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BCP3OAQUARzBEAiBFjEv1Lv8nkFfdJU0/JJHGJcGtZMCKGndPhfF01jPoPwIgb97psutmpVR1A60BsDjX96zpXLWE8g3pUDMWz0zkFUMBSDBFAiEAqBvDHFkWEXJfdqanzQJ25fUKXvjWUo/wa0otJAkBD1YCIGZhS3xBgLX/pHbmYg12ENLqGQIzIJrPPID3JFdTjq0VAUgwRQIhALJWK8oSsuTY59RjSbdNmboI+tmjsptwLGkWYio/GqA/AiAOSswTTc34QqumDI/qHcJLCQt3LTU9XheXOIo3MSuSxgHxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgABASBwaQMAAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FRzBEAiAEujxRerGoet/VhgYMeSFCkeuE8Z42OIXGx/ofrJ50/gIgLsbE5A0dlCIXXpckf35MBn9jiLVKD6tnLy1ZIj8FVe8BIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJHMEQCIEjLH1c9Rkq0wad3KqAxlpQasFjuN2gAf+mpWiazgxsnAiBl+7+NXJt8JFc5a+JNWz1f98gIwAGNOVPFo9vQJzZGhQEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0cwRAIgSKyvqLBOmZLbK72dTb9LdoUw01eQCBrj7Vxjgw1KHVECIEZVa67aNKIA4evyOq2e9C7+J91KkTA8GJst2eRUVskSAQEEIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BBfFTIQIvUztmfi6js24hlhyf6dyjQPvgr1IQFzqDrgM3qyCldiECa7U6mOgQvQ7mGg7RFkumwCR4bXZVTnk+IC3Gzpx4xOohAtW4p9ZqQf/bb0xT1hmUAi6Ia09FAB+xWLlckWTUX4yjIQMkt17q0sH5xg6K3rXnAJ/seimvzbMNgp2C0JVi/ouuhSEDLTT4kyIAgzSHvSlKohncvgALn5s9gkeZVBQwAJ8PpVEhA3Ro+OqZtsZHiDmLWtJUgMrQj0sNZb5UzjpV/SBrWuRyIQP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk1euAQcjIgAgdBDiqcx7V0LtGgDr8Co4bneNqt4doLQVuq7q8EAvTw0BCP3MAQUARzBEAiAEujxRerGoet/VhgYMeSFCkeuE8Z42OIXGx/ofrJ50/gIgLsbE5A0dlCIXXpckf35MBn9jiLVKD6tnLy1ZIj8FVe8BRzBEAiBIyx9XPUZKtMGndyqgMZaUGrBY7jdoAH/pqVoms4MbJwIgZfu/jVybfCRXOWviTVs9X/fICMABjTlTxaPb0Cc2RoUBRzBEAiBIrK+osE6ZktsrvZ1Nv0t2hTDTV5AIGuPtXGODDUodUQIgRlVrrto0ogDh6/I6rZ70Lv4n3UqRMDwYmy3Z5FRWyRIB8VMhAi9TO2Z+LqOzbiGWHJ/p3KNA++CvUhAXOoOuAzerIKV2IQJrtTqY6BC9DuYaDtEWS6bAJHhtdlVOeT4gLcbOnHjE6iEC1bin1mpB/9tvTFPWGZQCLohrT0UAH7FYuVyRZNRfjKMhAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FIQMtNPiTIgCDNIe9KUqiGdy+AAufmz2CR5lUFDAAnw+lUSEDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HIhA/ctPZZmOw6pmwrrDX8nPKsRqN43iF8d3cjZESrbhxaTV64AAA== diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..7d4ed98 --- /dev/null +++ b/web/index.html @@ -0,0 +1,152 @@ + + + + + +Bitcoin Proof of Reserves + + + +

Bitcoin Proof of Reserves

+

+ Verify a BIP-127 + proof of reserves PSBT. Everything runs in this tab: the PSBT is never uploaded anywhere. +

+ +
+
+ Proof +
+ + +
+
+ + +
+
+ + +
+
+ +
+ Lookup +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ +
+ + + + + + From 36e25bd0556a61e4ce6bfcb3983b0c5c43d3a2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20K=C3=B6hler?= Date: Wed, 16 Sep 2026 11:39:06 +0200 Subject: [PATCH 2/7] Build on the MSRV, and format with stable addr_of_mut! on a static mut needs an unsafe block on 1.78, which is what CI tests as the MSRV, so the exception slot is an UnsafeCell now. The fmt job only used nightly for format_code_in_doc_comments, which nothing here relies on. Also drops an unused dev-dependency, treats an emptied confirmations field as the default rather than zero, and notes that build.sh fetches Linux x86_64 tools. --- .github/workflows/cont_integration.yml | 6 ++---- Cargo.toml | 3 --- README.md | 3 ++- src/csupport.rs | 10 ++++++++-- web/app.js | 5 ++++- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 16da3e6..ada9a33 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -69,12 +69,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Set default toolchain - run: rustup default nightly + run: rustup default 1.83.0 - name: Set profile run: rustup set profile minimal - name: Add rustfmt run: rustup component add rustfmt - - name: Update toolchain - run: rustup update - name: Check fmt - run: cargo fmt --all -- --config format_code_in_doc_comments=true --check + run: cargo fmt --all -- --check diff --git a/Cargo.toml b/Cargo.toml index 4d83b21..7c9bad9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,9 +32,6 @@ getrandom = { version = "0.2", features = ["js"] } version = "0.3" features = ["Request", "RequestInit", "Response"] -[dev-dependencies] -bdk_wallet = { version = "1.0.0", features = ["std"] } - [profile.release] opt-level = 'z' # Optimize for size lto = true # Enable link-time optimization diff --git a/README.md b/README.md index 00a44b7..dc77500 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,8 @@ server that hides a UTXO makes a valid proof fail, which is the safe direction. ## Building Needs a Rust toolchain, `curl` and `node` (for the end to end test). Everything -else is fetched into `.tools/` on first build. +else is fetched into `.tools/` on first build. The fetched toolchain is the +Linux x86_64 build, so that is currently the only host `build.sh` supports. ```shell make build # produces dist/ diff --git a/src/csupport.rs b/src/csupport.rs index 65e57c9..0c03b06 100644 --- a/src/csupport.rs +++ b/src/csupport.rs @@ -125,11 +125,17 @@ pub extern "C" fn __cxa_atexit(_f: *mut c_void, _arg: *mut c_void, _dso: *mut c_ /// strings inside Bitcoin Core itself rather than on anything callers pass in, /// so trapping is the honest behaviour. The slot exists because the compiler /// constructs the exception object before handing it to `__cxa_throw`. -static mut EXCEPTION_SLOT: [u8; 256] = [0; 256]; +/// +/// `UnsafeCell` rather than `static mut`, so handing out the pointer needs no +/// unsafe block on any compiler this builds with. wasm is single threaded, so +/// the `Sync` claim costs nothing. +struct ExceptionSlot(core::cell::UnsafeCell<[u8; 256]>); +unsafe impl Sync for ExceptionSlot {} +static EXCEPTION_SLOT: ExceptionSlot = ExceptionSlot(core::cell::UnsafeCell::new([0; 256])); #[no_mangle] pub extern "C" fn __cxa_allocate_exception(_size: usize) -> *mut c_void { - core::ptr::addr_of_mut!(EXCEPTION_SLOT) as *mut c_void + EXCEPTION_SLOT.0.get() as *mut c_void } #[no_mangle] diff --git a/web/app.js b/web/app.js index 4672a90..5b622bb 100644 --- a/web/app.js +++ b/web/app.js @@ -109,7 +109,10 @@ form.addEventListener('submit', async (event) => { addresses: parseAddresses(document.getElementById('addresses').value), message: document.getElementById('message').value, proof_psbt: document.getElementById('psbt').value, - confirmations: Number(document.getElementById('confirmations').value), + // An emptied field means "the default", not zero confirmations. + confirmations: document.getElementById('confirmations').value === '' + ? null + : Number(document.getElementById('confirmations').value), esplora_url: document.getElementById('esplora').value.trim() || null, } From ba57d3c6da8d4995fb7ee9b7938376ad57b1aab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20K=C3=B6hler?= Date: Wed, 16 Sep 2026 11:42:22 +0200 Subject: [PATCH 3/7] Put the rustc badge back, at the MSRV CI actually checks --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dc77500..462c69c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@

MIT or Apache-2.0 Licensed CI Status + Rustc Version 1.78.0+

From 41bbaaa9a32c9afeafca9d58a6316edeece6bd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20K=C3=B6hler?= Date: Wed, 16 Sep 2026 12:03:09 +0200 Subject: [PATCH 4/7] Take bdk-reserves from crates.io now that the port is merged upstream The bdk_wallet 1.0 port went in as bitcoindevkit/bdk-reserves#39 and shipped as 3.0.0, so there is no reason to pin a branch on a fork any more. It pulls in bdk_wallet 3 and bitcoinconsensus 0.105, which checks signatures with the script interpreter from Bitcoin Core 25.1. bitcoinconsensus 0.105 does not use the libc crate, so the wasm-only stand-in goes, and with it the Cargo.lock juggling in build.sh that only existed to hide the patch. bdk_wallet 3 needs Rust 1.85, so that is the MSRV now. --- .cargo/wasm-patch.toml | 4 --- .github/workflows/cont_integration.yml | 6 ++--- .github/workflows/pages.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 8 ++++++ Cargo.lock | 34 +++++++++++++------------- Cargo.toml | 4 +-- README.md | 2 +- build.sh | 13 +--------- patches/libc/Cargo.toml | 22 ----------------- patches/libc/src/lib.rs | 3 --- 11 files changed, 34 insertions(+), 66 deletions(-) delete mode 100644 .cargo/wasm-patch.toml delete mode 100644 patches/libc/Cargo.toml delete mode 100644 patches/libc/src/lib.rs diff --git a/.cargo/wasm-patch.toml b/.cargo/wasm-patch.toml deleted file mode 100644 index d29b2f1..0000000 --- a/.cargo/wasm-patch.toml +++ /dev/null @@ -1,4 +0,0 @@ -# Passed to cargo as `--config .cargo/wasm-patch.toml` by build.sh, so the libc -# stand-in applies to the wasm build only and native `cargo test` is unaffected. -[patch.crates-io] -libc = { path = "patches/libc" } diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index ada9a33..3fe76e0 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -18,9 +18,9 @@ jobs: strategy: matrix: rust: - - version: 1.83.0 # STABLE + - version: 1.97.1 # STABLE clippy: true - - version: 1.78.0 # MSRV + - version: 1.85.0 # MSRV steps: - name: checkout uses: actions/checkout@v4 @@ -69,7 +69,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Set default toolchain - run: rustup default 1.83.0 + run: rustup default 1.97.1 - name: Set profile run: rustup set profile minimal - name: Add rustfmt diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d4ff591..8987dd4 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v4 - name: Set default toolchain - run: rustup default 1.83.0 + run: rustup default 1.97.1 - name: Add the wasm target run: rustup target add wasm32-unknown-unknown diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a52dda..9a589ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set default toolchain - run: rustup default 1.83.0 + run: rustup default 1.97.1 - name: Add the wasm target run: rustup target add wasm32-unknown-unknown - name: Build the static site diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e04258..ac06c41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,11 @@ The app now runs entirely in the browser. There is no backend left. checked against the txid requested, so the Esplora server cannot inflate the reported total. - Confirmation depth is configurable in the UI, still defaulting to 3. +- `bdk-reserves` comes from crates.io (3.0.0) now that its bdk_wallet 1.0 port + has been merged upstream, instead of a git branch on a fork. That brings + `bdk_wallet` 3 and `bitcoinconsensus` 0.105, so signatures are checked by the + script interpreter from Bitcoin Core 25.1, and lifts the minimum Rust + version to 1.85. - The page has a "Load example" button that fills in a real testnet proof, along with a note explaining that its UTXOs have since been spent. - Verification failures are explained in plain language rather than only as the @@ -43,6 +48,9 @@ The app now runs entirely in the browser. There is no backend left. ### Removed - The actix-web HTTP service and its `/proof` endpoint. +- The `libc` stand-in crate for the wasm build. `bitcoinconsensus` 0.105 does + not need it, and without the patch the wasm build no longer rewrites + `Cargo.lock`. ## [v0.1.10] diff --git a/Cargo.lock b/Cargo.lock index caf7e7e..bb0b902 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,8 +44,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bdk-reserves" -version = "1.0.0" -source = "git+https://github.com/AminaBank/bdk-reserves?branch=feature%2Fv1#10c2b54f11fc323ee6abc9dd30042b0df5f8b1f0" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4400d5b8ca95fc2baaaab7253491dccdcb60449ce22cf730d73f8a034da50938" dependencies = [ "bdk_wallet", "bitcoin-units", @@ -74,9 +75,9 @@ dependencies = [ [[package]] name = "bdk_chain" -version = "0.21.1" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4955734f97b2baed3f36d16ae7c203fdde31ae85391ac44ee3cbcaf0886db5ce" +checksum = "c290eff038799a8ac0c5a82b6160a9ca456baa299a6f22b262c771342d2846c0" dependencies = [ "bdk_core", "bitcoin", @@ -86,9 +87,9 @@ dependencies = [ [[package]] name = "bdk_core" -version = "0.4.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b545aea1efc090e4f71f1dd5468090d9f54c3de48002064c04895ef811fbe0b2" +checksum = "cb3028782f6bf14a6df987244333d34e6b272b5a40a53e4879ec2dfd82275a3a" dependencies = [ "bitcoin", "hashbrown", @@ -97,9 +98,9 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "1.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6362d350e13a94f68a102dafadbcc1500705016e4c2971b4c4e6f46054cb18" +checksum = "1284fb23acc3e3022673712b55f4d5ce7e38aadc2c49bbef830dc3935f0a3289" dependencies = [ "bdk_chain", "bitcoin", @@ -117,9 +118,9 @@ checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "bitcoin" -version = "0.32.5" +version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" +checksum = "9cf93e61f2dbc3e3c41234ca26a65e2c0b0975c52e0f069ab9893ebbede584d3" dependencies = [ "base58ck", "base64", @@ -172,12 +173,11 @@ dependencies = [ [[package]] name = "bitcoinconsensus" -version = "0.19.0-3" +version = "0.105.0+25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a8aa43b5cd02f856cb126a9af819e77b8910fdd74dd1407be649f2f5fe3a1b5" +checksum = "f260ac8fb2c621329013fc0ed371c940fcc512552dcbcb9095ed0179098c9e18" dependencies = [ "cc", - "libc", ] [[package]] @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08" dependencies = [ "arrayvec", ] @@ -317,9 +317,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniscript" -version = "12.3.0" +version = "12.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" +checksum = "b8343cc1ef1408bd9bdbf69f7aef47017dfab7e6349ec26fddf62e0e9fb5a4cf" dependencies = [ "bech32", "bitcoin", diff --git a/Cargo.toml b/Cargo.toml index 7c9bad9..249a873 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ license = "MIT OR Apache-2.0" crate-type = ["cdylib", "rlib"] [dependencies] -bdk_wallet = { version = "1.0.0", default-features = false, features = ["std"] } -bdk-reserves = { git = "https://github.com/AminaBank/bdk-reserves", branch = "feature/v1" } +bdk_wallet = { version = "3", default-features = false, features = ["std"] } +bdk-reserves = "3.0.0" serde = { version = "1", features = ["derive"] } serde_json = "1" log = "0.4" diff --git a/README.md b/README.md index 462c69c..658166b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

MIT or Apache-2.0 Licensed CI Status - Rustc Version 1.78.0+ + Rustc Version 1.85.0+

diff --git a/build.sh b/build.sh index 6c5338b..8940ed6 100755 --- a/build.sh +++ b/build.sh @@ -17,8 +17,6 @@ WASM_BINDGEN_VERSION="${WASM_BINDGEN_VERSION:-0.2.127}" # keep in step with Car TOOLS_DIR="${TOOLS_DIR:-$PWD/.tools}" WASI_SDK_PATH="${WASI_SDK_PATH:-$TOOLS_DIR/wasi-sdk-${WASI_SDK_VERSION}-x86_64-linux}" -LOCKFILE_BACKUP="$(mktemp)" - log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } if [ ! -d "$WASI_SDK_PATH" ]; then @@ -60,17 +58,8 @@ export CXXFLAGS_wasm32_unknown_unknown="--target=wasm32-wasip1 -fexceptions" export CXXSTDLIB_wasm32_unknown_unknown="c++" export RUSTFLAGS="-L $SYSROOT/lib/wasm32-wasip1 -C link-arg=-lc++abi" -# Patching libc turns it into a path dependency, which rewrites its entry in -# Cargo.lock. The committed lockfile describes the native build, so keep that -# the canonical one and put it back afterwards rather than letting the two -# builds fight over it. -if [ -f Cargo.lock ]; then - cp Cargo.lock "$LOCKFILE_BACKUP" - trap 'mv -f "$LOCKFILE_BACKUP" Cargo.lock 2>/dev/null || true' EXIT -fi - log "Building the wasm module" -cargo build --release --target wasm32-unknown-unknown --config .cargo/wasm-patch.toml +cargo build --release --target wasm32-unknown-unknown log "Generating JS bindings" rm -rf dist diff --git a/patches/libc/Cargo.toml b/patches/libc/Cargo.toml deleted file mode 100644 index 64c7f91..0000000 --- a/patches/libc/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -# Stand-in for the `libc` crate, applied only to wasm32-unknown-unknown builds -# (see .cargo/wasm-patch.toml). The real libc has no definitions for that -# target, but `bitcoinconsensus` is the only crate in the wasm dependency graph -# that needs it, and all it wants are a few C integer type aliases that -# core::ffi provides. -# -# Native builds do not use this - they get the real libc. -[package] -name = "libc" -version = "0.2.189" -edition = "2021" -publish = false - -[lib] -name = "libc" -path = "src/lib.rs" - -[features] -default = ["std"] -std = [] -extra_traits = [] -align = [] diff --git a/patches/libc/src/lib.rs b/patches/libc/src/lib.rs deleted file mode 100644 index 1f09e2c..0000000 --- a/patches/libc/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -#![no_std] - -pub use core::ffi::{c_char, c_int, c_long, c_schar, c_short, c_uchar, c_uint, c_ulong, c_ushort, c_void}; From ac95e1e2712502ef3247d51bca6656fc854537b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20K=C3=B6hler?= Date: Wed, 16 Sep 2026 12:03:09 +0200 Subject: [PATCH 5/7] Rebrand SEBA Bank to AMINA Bank Logo and link as in bitcoindevkit/bdk-reserves. The example proof's message still says SEBA: it is signed into the proof and cannot change. --- CHANGELOG.md | 3 +++ README.md | 6 +++--- static/amina_logo.png | Bin 0 -> 15757 bytes ...-bank-logo-bank-lange-green-ohne-tagline.png | Bin 19308 -> 0 bytes web/index.html | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 static/amina_logo.png delete mode 100644 static/seba-bank-logo-bank-lange-green-ohne-tagline.png diff --git a/CHANGELOG.md b/CHANGELOG.md index ac06c41..0d6cdd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ The app now runs entirely in the browser. There is no backend left. `bdk_wallet` 3 and `bitcoinconsensus` 0.105, so signatures are checked by the script interpreter from Bitcoin Core 25.1, and lifts the minimum Rust version to 1.85. +- SEBA Bank is AMINA Bank now, and the branding follows. The example proof's + message still names SEBA, because it is signed into the proof and cannot + change. - The page has a "Load example" button that fills in a real testnet proof, along with a note explaining that its UTXOs have since been spent. - Verification failures are explained in plain language rather than only as the diff --git a/README.md b/README.md index 658166b..486b1e1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@
- +

Proof of reserves for Bitcoin dev kit - browser app @@ -11,7 +11,7 @@

MIT or Apache-2.0 Licensed - CI Status + CI Status Rustc Version 1.85.0+

@@ -98,7 +98,7 @@ Pages serves the app from a subdirectory rather than a domain root, which is why everything in `web/` refers to its assets relatively. ## Sponsorship -The implementation of bdk-reserves-web was sponsored by SEBA Bank. +The implementation of bdk-reserves-web was sponsored by AMINA Bank. ## License diff --git a/static/amina_logo.png b/static/amina_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6d53e968c2819c8fe42e328abdff1bc64e80a4b0 GIT binary patch literal 15757 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 diff --git a/web/index.html b/web/index.html index 7d4ed98..93074a0 100644 --- a/web/index.html +++ b/web/index.html @@ -140,7 +140,7 @@

Bitcoin Proof of Reserves