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..3fe76e0 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -18,47 +18,49 @@ 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 - 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 @@ -67,12 +69,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Set default toolchain - run: rustup default nightly + run: rustup default 1.97.1 - 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/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..8987dd4 --- /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.97.1 + - 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..9a589ac 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.97.1 + - 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..3044087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,57 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [v0.3.0] + +### 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. +- `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. +- 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 + 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. +- 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 4d78fa9..c37378e 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,16 +42,11 @@ 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" -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", @@ -324,22 +56,27 @@ dependencies = [ [[package]] name = "bdk-reserves-web" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "actix-web", "bdk-reserves", - "bdk_electrum", "bdk_wallet", - "log", + "console_error_panic_hook", + "futures-util", + "getrandom", + "js-sys", "serde", + "serde-wasm-bindgen", "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[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", @@ -349,30 +86,20 @@ 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 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" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6362d350e13a94f68a102dafadbcc1500705016e4c2971b4c4e6f46054cb18" +checksum = "1284fb23acc3e3022673712b55f4d5ce7e38aadc2c49bbef830dc3935f0a3289" dependencies = [ "bdk_chain", "bitcoin", @@ -388,37 +115,14 @@ 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" +version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" +checksum = "9cf93e61f2dbc3e3c41234ca26a65e2c0b0975c52e0f069ab9893ebbede584d3" dependencies = [ "base58ck", - "base64 0.21.7", + "base64", "bech32", "bitcoin-internals", "bitcoin-io", @@ -468,49 +172,18 @@ 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]] -name = "bitflags" -version = "2.8.0" -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" +name = "bumpalo" +version = "3.20.3" 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 +191,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,722 +207,135 @@ 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.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08" 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.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d627e4feaf3009c10c8a0eb06d6ceb4ce1ff861849157fb35e8155d9706babb6" +checksum = "b8343cc1ef1408bd9bdbf69f7aef47017dfab7e6349ec26fddf62e0e9fb5a4cf" 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" - -[[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" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1283,210 +343,68 @@ 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" 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" + "zerocopy", +] [[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 +412,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 +433,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 +442,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 +476,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 +491,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 +502,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 +521,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 +605,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..fa4f5d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,35 @@ [package] name = "bdk-reserves-web" -version = "0.2.0" +version = "0.3.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 = "3", default-features = false, features = ["std"] } +bdk-reserves = "3.0.0" 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" -[dev-dependencies] +# 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"] [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/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..9d982a4 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,16 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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..486b1e1 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,16 @@
- +

- 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 + CI Status + Rustc Version 1.85.0+

@@ -27,18 +26,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. -## Sponsorship -The implementation of bdk-reserves-web was sponsored by SEBA Bank. +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. The fetched toolchain is the +Linux x86_64 build, so that is currently the only host `build.sh` supports. + +```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 AMINA Bank. ## License diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..8940ed6 --- /dev/null +++ b/build.sh @@ -0,0 +1,75 @@ +#!/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}" + +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" + +log "Building the wasm module" +cargo build --release --target wasm32-unknown-unknown + +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/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..0c03b06 --- /dev/null +++ b/src/csupport.rs @@ -0,0 +1,185 @@ +//! 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`. +/// +/// `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 { + EXCEPTION_SLOT.0.get() 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/static/amina_logo.png b/static/amina_logo.png new file mode 100644 index 0000000..6d53e96 Binary files /dev/null and b/static/amina_logo.png differ diff --git a/static/seba-bank-logo-bank-lange-green-ohne-tagline.png b/static/seba-bank-logo-bank-lange-green-ohne-tagline.png deleted file mode 100644 index 54f5643..0000000 Binary files a/static/seba-bank-logo-bank-lange-green-ohne-tagline.png and /dev/null differ 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..5b622bb --- /dev/null +++ b/web/app.js @@ -0,0 +1,160 @@ +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, + // 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, + } + + 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/862iIrAAAAAAAAQEKAAAAAAAAAAABUQEHAAABASAoOPwCAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDor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 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..93074a0 --- /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 +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ +
+ + + + + +