From 67ba32f42c3a7fc4bdeb7e72070297fc492b2f5e Mon Sep 17 00:00:00 2001 From: Buffrr Date: Fri, 17 Apr 2026 04:59:28 +0200 Subject: [PATCH 1/4] chore: set up release-plz and strict CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add release-plz config + workflow (runs on subspaces branch) and commitlint workflow; retarget release.yml to spaces_client-v* tags so binary tarballs attach to the release release-plz creates. - Normalize workspace metadata (version 0.1.0, edition 2024, MSRV 1.85, MIT, shared repo/homepage/authors) via workspace.package/dependencies inheritance. - Swap git deps for published crates: bdk_wallet → bdk_wallet_backport, spacedb → 0.1.2. - Tighten CI: lint (fmt + clippy -D warnings), docs (rustdoc -D warnings), MSRV 1.85 check, and no-std build matrix for borsh_utils, protocol, nums, sip7. - Clean up 670 fmt diffs, 138 clippy warnings, and 8 rustdoc warnings; fix pre-existing SLabelRef Display infinite-recursion; make spaces_nums genuinely no-std by gating bech32 string I/O behind the bech32 feature. --- .commitlintrc.yml | 21 + .github/workflows/ci.yml | 68 +- .github/workflows/commitlint.yml | 16 + .github/workflows/release-plz.yml | 49 + .github/workflows/release.yml | 37 +- Cargo.lock | 32 +- Cargo.toml | 24 +- borsh_utils/Cargo.toml | 10 +- borsh_utils/src/lib.rs | 4 +- checkpoint/Cargo.toml | 12 +- checkpoint/src/builder.rs | 35 +- checkpoint/src/integrity.rs | 2 +- checkpoint/src/lib.rs | 122 +- client/Cargo.toml | 22 +- client/src/app.rs | 36 +- client/src/auth.rs | 4 +- client/src/bin/space-cli.rs | 149 ++- client/src/bin/spaced.rs | 12 +- client/src/cbf.rs | 116 +- client/src/checker.rs | 40 +- client/src/client.rs | 109 +- client/src/config.rs | 21 +- client/src/format.rs | 119 +- client/src/lib.rs | 6 +- client/src/rpc.rs | 482 ++++---- client/src/rpc_schema.rs | 551 ++++++--- client/src/source.rs | 176 +-- client/src/spaces.rs | 26 +- client/src/store/chain.rs | 192 +++- client/src/store/index.rs | 47 +- client/src/store/mod.rs | 28 +- client/src/store/ptrs.rs | 92 +- client/src/store/spaces.rs | 56 +- client/src/wallets.rs | 230 ++-- client/tests/fetcher_tests.rs | 4 +- client/tests/integration_tests.rs | 286 +++-- client/tests/ptr_tests.rs | 1792 +++++++++++++++++++++-------- nums/Cargo.toml | 14 +- nums/src/constants.rs | 16 +- nums/src/lib.rs | 64 +- nums/src/num_id.rs | 53 +- nums/src/snumeric.rs | 57 +- protocol/Cargo.toml | 12 +- protocol/src/constants.rs | 45 +- protocol/src/hasher.rs | 6 +- protocol/src/lib.rs | 49 +- protocol/src/prepare.rs | 39 +- protocol/src/script.rs | 41 +- protocol/src/slabel.rs | 37 +- protocol/src/sname.rs | 43 +- protocol/src/validate.rs | 40 +- release-plz.toml | 106 ++ sip7/Cargo.toml | 12 +- sip7/src/lib.rs | 236 ++-- testutil/Cargo.toml | 15 +- testutil/build.rs | 2 +- testutil/src/bin/db_dump.rs | 32 +- testutil/src/lib.rs | 42 +- testutil/src/spaced.rs | 64 +- wallet/Cargo.toml | 14 +- wallet/src/address.rs | 4 +- wallet/src/builder.rs | 126 +- wallet/src/export.rs | 2 +- wallet/src/lib.rs | 235 ++-- wallet/src/nostr.rs | 15 +- wallet/src/rusqlite_impl.rs | 8 +- wallet/src/tx_event.rs | 151 ++- 67 files changed, 4354 insertions(+), 2254 deletions(-) create mode 100644 .commitlintrc.yml create mode 100644 .github/workflows/commitlint.yml create mode 100644 .github/workflows/release-plz.yml create mode 100644 release-plz.toml diff --git a/.commitlintrc.yml b/.commitlintrc.yml new file mode 100644 index 0000000..320a584 --- /dev/null +++ b/.commitlintrc.yml @@ -0,0 +1,21 @@ +extends: + - '@commitlint/config-conventional' + +rules: + # Allow slightly longer subjects; we have descriptive messages. + header-max-length: [2, always, 100] + # Enforce lowercase type (feat, fix, ...) and allow common scopes. + type-enum: + - 2 + - always + - - build + - chore + - ci + - docs + - feat + - fix + - perf + - refactor + - revert + - style + - test \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ef1faa..dd2fb95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,28 +10,60 @@ on: - main - subspaces +env: + CARGO_TERM_COLOR: always + jobs: - run-tests: + test: + name: Test runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --verbose - - name: Set up Rust - uses: actions-rs/toolchain@v1 + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - profile: minimal - override: true + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all -- --check + - run: cargo clippy --all-targets --all-features -- -D warnings - - name: Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-registry- + docs: + name: Docs + runs-on: ubuntu-latest + env: + RUSTDOCFLAGS: -D warnings + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo doc --no-deps --all-features + + msrv: + name: MSRV (1.85) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.85.0 + - uses: Swatinem/rust-cache@v2 + - run: cargo check --all-features - - name: Run tests - run: cargo test + no-std: + name: no_std build (${{ matrix.crate }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + crate: [borsh_utils, spaces_protocol, spaces_nums, sip7] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo check -p ${{ matrix.crate }} --no-default-features \ No newline at end of file diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..57918ab --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,16 @@ +name: Commitlint + +on: + pull_request: + types: [opened, reopened, edited, synchronize] + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v6 + with: + configFile: .commitlintrc.yml \ No newline at end of file diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 0000000..1a0548d --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,49 @@ +name: Release-plz + +permissions: + pull-requests: write + contents: write + +on: + push: + branches: [subspaces] + +jobs: + # Opens / updates the "release PR" that bumps versions and edits the CHANGELOG. + release-plz-pr: + name: Release-plz PR + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'buffrr' }} + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + # Tags + publishes to crates.io when a release commit lands on main. + release-plz-release: + name: Release-plz publish + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'buffrr' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7be1853..fbef3bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,9 @@ -name: Build binaries and create release +name: Build binaries and upload to release on: push: tags: - - 'v*.*.*' + - 'spaces_client-v*' jobs: release: @@ -24,10 +24,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Get the tag name + - name: Extract version from tag id: get_tag - run: | + run: | + # tag format: spaces_client-v0.1.0 → VERSION=0.1.0, TAG= echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV + echo "VERSION=${GITHUB_REF##*-v}" >> $GITHUB_ENV - name: Set up Rust uses: actions-rust-lang/setup-rust-toolchain@v1 @@ -44,7 +46,7 @@ jobs: env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc run: | - cargo build --release --target ${{ matrix.target }} + cargo build --release --target ${{ matrix.target }} -p spaces_client - name: Get OS and architecture run: | @@ -53,23 +55,16 @@ jobs: - name: Create release archive run: | - mkdir -p spaces-${{ env.TAG }}-${{ env.OS }}-${{ env.ARCH }} - cp target/${{ matrix.target }}/release/spaced spaces-${{ env.TAG }}-${{ env.OS }}-${{ env.ARCH }}/spaced - cp target/${{ matrix.target }}/release/space-cli spaces-${{ env.TAG }}-${{ env.OS }}-${{ env.ARCH }}/space-cli - tar -czf spaces-${{ env.TAG }}-${{ env.OS }}-${{ env.ARCH }}.tar.gz spaces-${{ env.TAG }}-${{ env.OS }}-${{ env.ARCH }} + mkdir -p spaces-v${{ env.VERSION }}-${{ env.OS }}-${{ env.ARCH }} + cp target/${{ matrix.target }}/release/spaced spaces-v${{ env.VERSION }}-${{ env.OS }}-${{ env.ARCH }}/spaced + cp target/${{ matrix.target }}/release/space-cli spaces-v${{ env.VERSION }}-${{ env.OS }}-${{ env.ARCH }}/space-cli + tar -czf spaces-v${{ env.VERSION }}-${{ env.OS }}-${{ env.ARCH }}.tar.gz spaces-v${{ env.VERSION }}-${{ env.OS }}-${{ env.ARCH }} - - name: Create GitHub Release - id: create_release + - name: Upload artifacts to existing release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ env.TAG }} # Dynamically use the pushed tag - name: Release ${{ env.TAG }} # Use the tag for the release name - body: | - Spaces release of version ${{ env.TAG }}. - draft: false - prerelease: false - files: | - spaces-${{ env.TAG }}-${{ env.OS }}-${{ env.ARCH }}.tar.gz - make_latest: true + tag_name: ${{ env.TAG }} + files: | + spaces-v${{ env.VERSION }}-${{ env.OS }}-${{ env.ARCH }}.tar.gz env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a422362..5b54a7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,9 +175,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] -name = "bdk_chain" +name = "bdk_chain_backport" version = "0.21.0" -source = "git+https://github.com/buffrr/bdk.git?rev=43bca8643dec6fdda99e4a29bf88709729af349e#43bca8643dec6fdda99e4a29bf88709729af349e" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d580eb55991b7a2415d71d195935f4f88b138257a5e7756724d77974c2f15153" dependencies = [ "bdk_core", "bitcoin", @@ -188,8 +189,9 @@ dependencies = [ [[package]] name = "bdk_core" -version = "0.4.0" -source = "git+https://github.com/buffrr/bdk.git?rev=43bca8643dec6fdda99e4a29bf88709729af349e#43bca8643dec6fdda99e4a29bf88709729af349e" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b545aea1efc090e4f71f1dd5468090d9f54c3de48002064c04895ef811fbe0b2" dependencies = [ "bitcoin", "hashbrown 0.14.5", @@ -197,11 +199,12 @@ dependencies = [ ] [[package]] -name = "bdk_wallet" +name = "bdk_wallet_backport" version = "1.0.0-beta.6" -source = "git+https://github.com/buffrr/bdk.git?rev=43bca8643dec6fdda99e4a29bf88709729af349e#43bca8643dec6fdda99e4a29bf88709729af349e" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf2fadfbc496ddc97a19bc99196abafe6d393eb0f6e51f36aa6cf93b9aeb8db" dependencies = [ - "bdk_chain", + "bdk_chain_backport", "bip39", "bitcoin", "miniscript", @@ -2629,8 +2632,9 @@ dependencies = [ [[package]] name = "spacedb" -version = "0.0.12" -source = "git+https://github.com/spacesprotocol/spacedb#43f23e78e4e5fffb8d89661a4b7f39ab43a5a644" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a48cda82e951391df9d0a54c96f8b04e117ff39abad5359b181cb71c80798d77" dependencies = [ "bincode", "borsh", @@ -2661,7 +2665,7 @@ dependencies = [ [[package]] name = "spaces_client" -version = "0.0.7" +version = "0.1.0" dependencies = [ "anyhow", "assert_cmd", @@ -2712,7 +2716,7 @@ dependencies = [ [[package]] name = "spaces_protocol" -version = "0.0.7" +version = "0.1.0" dependencies = [ "bitcoin", "borsh", @@ -2723,7 +2727,7 @@ dependencies = [ [[package]] name = "spaces_testutil" -version = "0.0.1" +version = "0.1.0" dependencies = [ "anyhow", "assert_cmd", @@ -2740,10 +2744,10 @@ dependencies = [ [[package]] name = "spaces_wallet" -version = "0.0.7" +version = "0.1.0" dependencies = [ "anyhow", - "bdk_wallet", + "bdk_wallet_backport", "bech32", "bitcoin", "borsh", diff --git a/Cargo.toml b/Cargo.toml index 5b46dc5..945e893 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,12 +2,31 @@ resolver = "2" members = ["borsh_utils", "checkpoint", "client", "protocol", "testutil", "wallet", "nums", "sip7"] +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +license = "MIT" +repository = "https://github.com/buffrr/spaces" +homepage = "https://spacesprotocol.org" +authors = ["Buffrr "] + [workspace.dependencies] +# Internal crates +borsh_utils = { path = "borsh_utils", version = "0.1.0", default-features = false } +spaces_protocol = { path = "protocol", version = "0.1.0", default-features = false } +spaces_nums = { path = "nums", version = "0.1.0", default-features = false } +sip7 = { path = "sip7", version = "0.1.0", default-features = false } +spaces_wallet = { path = "wallet", version = "0.1.0", default-features = false } +spaces_client = { path = "client", version = "0.1.0", default-features = false } +spaces_testutil = { path = "testutil", version = "0.1.0", default-features = false } + +# External anyhow = "1.0" hex = "0.4" serde_json = "1.0" tokio = { version = "1", features = ["full"] } -spacedb = { git = "https://github.com/spacesprotocol/spacedb", features = ["hash-idx"] } +spacedb = { version = "0.1.2", features = ["hash-idx"] } base64 = "0.22" rand = "0.8" log = "0.4" @@ -24,7 +43,8 @@ tower = "0.4" hyper = "0.14" secp256k1 = "0.29" bech32 = "0.11" -bdk_wallet = { git = "https://github.com/buffrr/bdk.git", rev = "43bca8643dec6fdda99e4a29bf88709729af349e", features = ["keys-bip39", "rusqlite"] } +# temporary backport of bdk for the double-spend issue until we migrate to bdk v3.0 +bdk_wallet = { package = "bdk_wallet_backport", version = "1.0.0-beta.6", features = ["keys-bip39", "rusqlite"] } # Dev/test dependencies assert_cmd = "2.0" diff --git a/borsh_utils/Cargo.toml b/borsh_utils/Cargo.toml index 1407b9f..f9a7f82 100644 --- a/borsh_utils/Cargo.toml +++ b/borsh_utils/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "borsh_utils" -version = "0.1.0" -edition = "2021" +description = "Borsh serialization helpers for Bitcoin primitives used by the Spaces protocol." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +authors.workspace = true [dependencies] bitcoin = { version = "0.32.8", features = ["serde"], default-features = false } diff --git a/borsh_utils/src/lib.rs b/borsh_utils/src/lib.rs index 5696fac..9c787b5 100644 --- a/borsh_utils/src/lib.rs +++ b/borsh_utils/src/lib.rs @@ -9,8 +9,8 @@ extern crate alloc; use alloc::vec; use bitcoin::hashes::Hash; -use bitcoin::{secp256k1::schnorr, Amount, BlockHash, OutPoint, ScriptBuf, Txid}; -use borsh::{io, BorshDeserialize, BorshSerialize}; +use bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, Txid, secp256k1::schnorr}; +use borsh::{BorshDeserialize, BorshSerialize, io}; /// Serialize a Txid pub fn serialize_txid(txid: &Txid, writer: &mut W) -> io::Result<()> { diff --git a/checkpoint/Cargo.toml b/checkpoint/Cargo.toml index 16bf506..022932a 100644 --- a/checkpoint/Cargo.toml +++ b/checkpoint/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "spaces_checkpoint" -version = "0.1.0" -edition = "2024" +description = "Checkpoint loader and builder for the Spaces protocol." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +authors.workspace = true [[bin]] name = "checkpoint-builder" @@ -20,7 +26,7 @@ serde_json = "1" anyhow = { version = "1", optional = true } clap = { version = "4", features = ["derive"], optional = true } spacedb = { workspace = true, optional = true } -spaces_protocol = { path = "../protocol", features = ["std"], optional = true } +spaces_protocol = { workspace = true, features = ["std"], optional = true } [dev-dependencies] tempfile = "3" diff --git a/checkpoint/src/builder.rs b/checkpoint/src/builder.rs index 2affb19..7bb7f27 100644 --- a/checkpoint/src/builder.rs +++ b/checkpoint/src/builder.rs @@ -1,8 +1,13 @@ +use clap::Parser; use std::path::{Path, PathBuf}; use std::process::Command; -use clap::Parser; -fn s3_cp(local: &Path, dest: &str, endpoint_url: Option<&str>, profile: Option<&str>) -> anyhow::Result<()> { +fn s3_cp( + local: &Path, + dest: &str, + endpoint_url: Option<&str>, + profile: Option<&str>, +) -> anyhow::Result<()> { eprintln!("Uploading to {}...", dest); let mut cmd = Command::new("aws"); cmd.args(["s3", "cp"]); @@ -16,7 +21,8 @@ fn s3_cp(local: &Path, dest: &str, endpoint_url: Option<&str>, profile: Option<& } let status = cmd.status().map_err(|e| { anyhow::anyhow!( - "failed to run `aws` CLI: {} — install from https://aws.amazon.com/cli/", e + "failed to run `aws` CLI: {} — install from https://aws.amazon.com/cli/", + e ) })?; if !status.success() { @@ -65,8 +71,11 @@ fn main() -> anyhow::Result<()> { let digest = spaces_checkpoint::build_checkpoint(&args.data_dir, &output)?; - eprintln!("\nArchive: {} ({} bytes)", output.display(), - std::fs::metadata(&output)?.len()); + eprintln!( + "\nArchive: {} ({} bytes)", + output.display(), + std::fs::metadata(&output)?.len() + ); eprintln!("SHA-256: {}", hex::encode(digest)); spaces_checkpoint::write_integrity(height, &block_hash, &digest)?; @@ -76,7 +85,12 @@ fn main() -> anyhow::Result<()> { let bucket = bucket.trim_end_matches('/'); // Upload the archive - s3_cp(&output, &format!("{}/{}", bucket, filename), args.endpoint_url.as_deref(), args.profile.as_deref())?; + s3_cp( + &output, + &format!("{}/{}", bucket, filename), + args.endpoint_url.as_deref(), + args.profile.as_deref(), + )?; // Write and upload latest.json let latest = spaces_checkpoint::Checkpoint { @@ -86,10 +100,15 @@ fn main() -> anyhow::Result<()> { }; let latest_path = args.output_dir.join("latest.json"); std::fs::write(&latest_path, serde_json::to_string_pretty(&latest)?)?; - s3_cp(&latest_path, &format!("{}/latest.json", bucket), args.endpoint_url.as_deref(), args.profile.as_deref())?; + s3_cp( + &latest_path, + &format!("{}/latest.json", bucket), + args.endpoint_url.as_deref(), + args.profile.as_deref(), + )?; eprintln!("Upload complete."); } Ok(()) -} \ No newline at end of file +} diff --git a/checkpoint/src/integrity.rs b/checkpoint/src/integrity.rs index ab60e9f..4492ea4 100644 --- a/checkpoint/src/integrity.rs +++ b/checkpoint/src/integrity.rs @@ -5,4 +5,4 @@ pub fn checkpoint() -> super::Checkpoint { block_hash: "000000000000000000012b40cac64a5fb5417cff15dc01461f99a76fc9643568".to_string(), digest: "e98c1999a3b98b517b174b3add778fb55f0fe63e0795e9f85952edc00a919f58".to_string(), } -} \ No newline at end of file +} diff --git a/checkpoint/src/lib.rs b/checkpoint/src/lib.rs index 8bcbc01..d7d2510 100644 --- a/checkpoint/src/lib.rs +++ b/checkpoint/src/lib.rs @@ -1,10 +1,10 @@ pub mod integrity; +use flate2::read::GzDecoder; +use sha2::{Digest, Sha256}; use std::fmt; use std::io::{Cursor, Read}; use std::path::Path; -use flate2::read::GzDecoder; -use sha2::{Sha256, Digest}; pub const CHECKPOINT_BASE_URL: &str = "https://checkpoints.spacesprotocol.org"; @@ -30,9 +30,10 @@ impl Checkpoint { let bytes = hex::decode(&self.digest) .map_err(|e| CheckpointError::Unavailable(format!("invalid digest hex: {}", e)))?; if bytes.len() != 32 { - return Err(CheckpointError::Unavailable( - format!("invalid digest length: {}", bytes.len()), - )); + return Err(CheckpointError::Unavailable(format!( + "invalid digest length: {}", + bytes.len() + ))); } let mut arr = [0u8; 32]; arr.copy_from_slice(&bytes); @@ -41,7 +42,11 @@ impl Checkpoint { /// Build the download URL for this checkpoint. pub fn url(&self, base_url: &str) -> String { - format!("{}/checkpoint-{}.tar.gz", base_url.trim_end_matches('/'), self.height) + format!( + "{}/checkpoint-{}.tar.gz", + base_url.trim_end_matches('/'), + self.height + ) } } @@ -53,7 +58,9 @@ pub fn fetch_latest(base_url: &str) -> Result, CheckpointErro Ok(r) => r, Err(_) => return Ok(None), }; - let body: String = response.into_body().read_to_string() + let body: String = response + .into_body() + .read_to_string() .map_err(|e| CheckpointError::Unavailable(e.to_string()))?; let latest: Checkpoint = serde_json::from_str(&body) .map_err(|e| CheckpointError::Unavailable(format!("invalid latest.json: {}", e)))?; @@ -80,8 +87,11 @@ impl fmt::Display for CheckpointError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CheckpointError::Unavailable(msg) => write!(f, "checkpoint unavailable: {}", msg), - CheckpointError::DigestMismatch { expected, got } => - write!(f, "checkpoint hash mismatch: expected {}, got {}", expected, got), + CheckpointError::DigestMismatch { expected, got } => write!( + f, + "checkpoint hash mismatch: expected {}, got {}", + expected, got + ), CheckpointError::Extract(e) => write!(f, "failed to extract checkpoint: {}", e), CheckpointError::Io(e) => write!(f, "io error: {}", e), } @@ -121,7 +131,9 @@ pub fn ensure_checkpoint( let delay = std::time::Duration::from_secs(1 << attempt); tracing::warn!( "checkpoint download attempt {}/{} failed, retrying in {:?}...", - attempt, MAX_RETRIES, delay + attempt, + MAX_RETRIES, + delay ); std::thread::sleep(delay); } @@ -134,7 +146,8 @@ pub fn ensure_checkpoint( if attempt == MAX_RETRIES { tracing::warn!( "checkpoint unavailable after {} attempts: {} — falling back to full sync", - MAX_RETRIES + 1, e + MAX_RETRIES + 1, + e ); return Ok(false); } @@ -153,10 +166,13 @@ fn download_and_verify( ) -> Result, CheckpointError> { tracing::info!("downloading checkpoint from {}", url); - let response = ureq::get(url).call() + let response = ureq::get(url) + .call() .map_err(|e| CheckpointError::Unavailable(e.to_string()))?; - let total = response.headers().get("content-length") + let total = response + .headers() + .get("content-length") .and_then(|v| v.to_str().ok()) .and_then(|v| v.parse::().ok()) .unwrap_or(0); @@ -165,7 +181,8 @@ fn download_and_verify( let mut data = Vec::with_capacity(total as usize); let mut buf = [0u8; 64 * 1024]; loop { - let n = reader.read(&mut buf) + let n = reader + .read(&mut buf) .map_err(|e| CheckpointError::Unavailable(e.to_string()))?; if n == 0 { break; @@ -194,7 +211,9 @@ fn extract(target_dir: &Path, data: &[u8]) -> Result<(), CheckpointError> { tracing::info!("extracting checkpoint..."); let decoder = GzDecoder::new(Cursor::new(data)); let mut archive = tar::Archive::new(decoder); - archive.unpack(target_dir).map_err(CheckpointError::Extract)?; + archive + .unpack(target_dir) + .map_err(CheckpointError::Extract)?; tracing::info!("checkpoint extracted to {:?}", target_dir); Ok(()) } @@ -203,8 +222,8 @@ fn extract(target_dir: &Path, data: &[u8]) -> Result<(), CheckpointError> { /// by inspecting the latest snapshot metadata in root.sdb. #[cfg(feature = "cli")] pub fn read_tip(data_dir: &Path) -> anyhow::Result<(String, u32)> { - use spacedb::db::Database; use spacedb::Sha256Hasher; + use spacedb::db::Database; use spaces_protocol::constants::ChainAnchor; let db_path = data_dir.join("root.sdb"); @@ -214,14 +233,20 @@ pub fn read_tip(data_dir: &Path) -> anyhow::Result<(String, u32)> { let config = spacedb::Configuration::standard(); let db: Database = Database::open_with_config( - db_path.to_str().ok_or_else(|| anyhow::anyhow!("invalid path"))?, + db_path + .to_str() + .ok_or_else(|| anyhow::anyhow!("invalid path"))?, config, )?; - let snapshot = db.iter().next() + let snapshot = db + .iter() + .next() .ok_or_else(|| anyhow::anyhow!("no snapshots in root.sdb"))?; let snapshot = snapshot?; - let anchor: ChainAnchor = snapshot.metadata().try_into() + let anchor: ChainAnchor = snapshot + .metadata() + .try_into() .map_err(|_| anyhow::anyhow!("could not read snapshot metadata"))?; Ok((anchor.hash.to_string(), anchor.height)) @@ -230,12 +255,9 @@ pub fn read_tip(data_dir: &Path) -> anyhow::Result<(String, u32)> { /// Build a checkpoint archive from a spaced data directory. /// Returns the SHA-256 digest of the produced archive. #[cfg(feature = "cli")] -pub fn build_checkpoint( - data_dir: &Path, - output: &Path, -) -> anyhow::Result<[u8; 32]> { - use flate2::write::GzEncoder; +pub fn build_checkpoint(data_dir: &Path, output: &Path) -> anyhow::Result<[u8; 32]> { use flate2::Compression; + use flate2::write::GzEncoder; use std::fs::File; for name in CHECKPOINT_FILES { @@ -274,15 +296,35 @@ pub fn checkpoint() -> super::Checkpoint {{ digest: "{}".to_string(), }} }}"#, - height, block_hash, hex::encode(digest), + height, + block_hash, + hex::encode(digest), ) } +#[cfg(feature = "cli")] +const INTEGRITY_PATH: &str = "checkpoint/src/integrity.rs"; + +/// Write the checkpoint constant to integrity.rs. +/// Must be run from the workspace root. +#[cfg(feature = "cli")] +pub fn write_integrity(height: u32, block_hash: &str, digest: &[u8; 32]) -> anyhow::Result<()> { + let path = Path::new(INTEGRITY_PATH); + if !path.exists() { + anyhow::bail!( + "{} not found — run checkpoint-builder from the workspace root", + INTEGRITY_PATH + ); + } + std::fs::write(path, format_integrity_file(height, block_hash, digest))?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; - use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; + use std::sync::atomic::{AtomicU64, Ordering}; #[test] fn download_reports_progress() { @@ -307,10 +349,16 @@ mod tests { let applied = ensure_checkpoint(dir.path(), &url, &digest, Some(&*progress)).unwrap(); assert!(applied, "checkpoint should be applied"); - assert!(call_count.load(Ordering::SeqCst) > 1, "progress should be called multiple times"); + assert!( + call_count.load(Ordering::SeqCst) > 1, + "progress should be called multiple times" + ); let total = last_total.load(Ordering::SeqCst); - assert!(total > 0, "total bytes should be reported from content-length"); + assert!( + total > 0, + "total bytes should be reported from content-length" + ); let downloaded = last_downloaded.load(Ordering::SeqCst); assert_eq!(downloaded, total, "final downloaded should equal total"); @@ -321,21 +369,3 @@ mod tests { } } } - -#[cfg(feature = "cli")] -const INTEGRITY_PATH: &str = "checkpoint/src/integrity.rs"; - -/// Write the checkpoint constant to integrity.rs. -/// Must be run from the workspace root. -#[cfg(feature = "cli")] -pub fn write_integrity(height: u32, block_hash: &str, digest: &[u8; 32]) -> anyhow::Result<()> { - let path = Path::new(INTEGRITY_PATH); - if !path.exists() { - anyhow::bail!( - "{} not found — run checkpoint-builder from the workspace root", - INTEGRITY_PATH - ); - } - std::fs::write(path, format_integrity_file(height, block_hash, digest))?; - Ok(()) -} \ No newline at end of file diff --git a/client/Cargo.toml b/client/Cargo.toml index 502db51..45082a9 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "spaces_client" -version = "0.0.7" -edition = "2021" +description = "Spaces client (spaced) and CLI (space-cli) for the Spaces Protocol." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +authors.workspace = true [[bin]] name = "space-cli" @@ -20,12 +26,12 @@ required-features = ["schema"] path = "src/lib.rs" [dependencies] -spaces_wallet = { path = "../wallet" } -spaces_protocol = { path = "../protocol", features = ["std"] } -spaces_nums= { path = "../nums", features = ["std"] } -sip7 = { path = "../sip7", features = ["serde"] } +spaces_wallet = { workspace = true } +spaces_protocol = { workspace = true, features = ["std"] } +spaces_nums = { workspace = true, features = ["std"] } +sip7 = { workspace = true, features = ["serde"] } spacedb = { workspace = true } -borsh_utils = { path = "../borsh_utils" } +borsh_utils = { workspace = true } tokio = { workspace = true, features = ["signal"] } anyhow = { workspace = true } @@ -56,4 +62,4 @@ schema = ["schemars"] [dev-dependencies] assert_cmd = { workspace = true } predicates = { workspace = true } -spaces_testutil = { path = "../testutil" } +spaces_testutil = { workspace = true } diff --git a/client/src/app.rs b/client/src/app.rs index dc219a2..38689cb 100644 --- a/client/src/app.rs +++ b/client/src/app.rs @@ -1,14 +1,14 @@ -use std::path::PathBuf; -use std::sync::Arc; -use anyhow::anyhow; -use tokio::sync::{broadcast, mpsc}; -use tokio::task::{JoinHandle, JoinSet}; use crate::config::Args; use crate::rpc::{AsyncChainState, RpcServerImpl, WalletLoadRequest, WalletManager}; use crate::source::{BitcoinBlockSource, BitcoinRpc}; use crate::spaces::Spaced; -use crate::store::chain::{Chain}; +use crate::store::chain::Chain; use crate::wallets::RpcWallet; +use anyhow::anyhow; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::{broadcast, mpsc}; +use tokio::task::{JoinHandle, JoinSet}; pub struct App { shutdown: broadcast::Sender<()>, @@ -23,7 +23,12 @@ impl App { } } - async fn setup_rpc_wallet(&mut self, spaced: &Spaced, rx: mpsc::Receiver, cbf: bool) { + async fn setup_rpc_wallet( + &mut self, + spaced: &Spaced, + rx: mpsc::Receiver, + cbf: bool, + ) { let wallet_service = RpcWallet::service( spaced.network, spaced.rpc.clone(), @@ -31,7 +36,7 @@ impl App { rx, self.shutdown.clone(), spaced.num_workers, - cbf + cbf, ); self.services.spawn(async move { @@ -59,7 +64,7 @@ impl App { chain_state, self.shutdown.subscribe(), ) - .await; + .await; self.services.spawn(async { async_chain_state_handle @@ -79,7 +84,8 @@ impl App { .map_err(|e| anyhow!("RPC Server error: {}", e)) }); - self.setup_rpc_wallet(spaced, wallet_loader_rx, spaced.cbf).await; + self.setup_rpc_wallet(spaced, wallet_loader_rx, spaced.cbf) + .await; } async fn setup_sync_service(&mut self, mut spaced: Spaced) { @@ -123,15 +129,7 @@ async fn create_async_store( let async_store = AsyncChainState::new(tx); let client = reqwest::Client::new(); let handle = tokio::spawn(async move { - AsyncChainState::handler( - &client, - rpc, - anchors, - state, - rx, - shutdown, - ) - .await + AsyncChainState::handler(&client, rpc, anchors, state, rx, shutdown).await }); (async_store, handle) } diff --git a/client/src/auth.rs b/client/src/auth.rs index 0ab9559..45836b5 100644 --- a/client/src/auth.rs +++ b/client/src/auth.rs @@ -1,5 +1,5 @@ use base64::Engine; -use hyper::{http::HeaderValue, Body, HeaderMap, Request, Response, StatusCode}; +use hyper::{Body, HeaderMap, Request, Response, StatusCode, http::HeaderValue}; use jsonrpsee::{ core::ClientError, http_client::{HttpClient, HttpClientBuilder}, @@ -51,7 +51,7 @@ impl BasicAuth { .get("authorization") .and_then(|h| h.to_str().ok()) .and_then(|s| s.strip_prefix("Basic ")) - .map_or(false, |token| token == self.token.as_ref()) + .is_some_and(|token| token == self.token.as_ref()) } fn unauthorized_response() -> Response { diff --git a/client/src/bin/space-cli.rs b/client/src/bin/space-cli.rs index 4faa7e1..ef9ae1d 100644 --- a/client/src/bin/space-cli.rs +++ b/client/src/bin/space-cli.rs @@ -1,41 +1,39 @@ extern crate core; -use std::{ - fs, io, - io::Write, - path::PathBuf, -}; -use std::str::FromStr; use anyhow::anyhow; use clap::{Parser, Subcommand}; use colored::{Color, Colorize}; use jsonrpsee::{ - core::{client::Error, ClientError}, + core::{ClientError, client::Error}, http_client::HttpClient, }; +use spaces_client::rpc::{ + CommitParams, CreateNumParams, DelegateParams, OperateParams, SetFallbackParams, +}; +use spaces_client::store::Sha256; use spaces_client::{ auth::{auth_token_from_cookie, auth_token_from_creds, http_client_with_auth}, - config::{default_cookie_path, default_spaces_rpc_port, ExtendedNetwork}, + config::{ExtendedNetwork, default_cookie_path, default_spaces_rpc_port}, format::{ - print_error_rpc_response, print_list_bidouts, print_list_nums_response, + Format, print_error_rpc_response, print_list_bidouts, print_list_nums_response, print_list_spaces_response, print_list_transactions, print_list_unspent, - print_list_wallets, print_server_info, print_wallet_balance_response, - print_wallet_info, print_wallet_response, Format, + print_list_wallets, print_server_info, print_wallet_balance_response, print_wallet_info, + print_wallet_response, }, rpc::{ - BidParams, OpenParams, RegisterParams, RpcClient, RpcWalletRequest, - RpcWalletTxBuilder, SendCoinsParams, Subject, TransferSpacesParams, + BidParams, OpenParams, RegisterParams, RpcClient, RpcWalletRequest, RpcWalletTxBuilder, + SendCoinsParams, Subject, TransferSpacesParams, }, wallets::{AddressKind, WalletResponse}, }; -use spaces_client::rpc::{CommitParams, CreateNumParams, DelegateParams, OperateParams, SetFallbackParams}; -use spaces_client::store::Sha256; +use spaces_nums::num_id::NumId; use spaces_protocol::bitcoin::{Amount, FeeRate, OutPoint, Txid}; use spaces_protocol::slabel::SLabel; -use spaces_nums::num_id::NumId; -use spaces_wallet::{bitcoin::secp256k1::schnorr::Signature, export::WalletExport, Listing}; -use spaces_wallet::bitcoin::hashes::sha256; use spaces_wallet::bitcoin::ScriptBuf; +use spaces_wallet::bitcoin::hashes::sha256; +use spaces_wallet::{Listing, bitcoin::secp256k1::schnorr::Signature, export::WalletExport}; +use std::str::FromStr; +use std::{fs, io, io::Write, path::PathBuf}; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -479,7 +477,7 @@ impl SpaceCli { Self { wallet: args.wallet.clone(), format: args.output_format, - dust: args.dust.map(|d| Amount::from_sat(d)), + dust: args.dust.map(Amount::from_sat), force: args.force, skip_tx_check: args.skip_tx_check, network: args.chain, @@ -538,7 +536,7 @@ async fn main() -> anyhow::Result<()> { match result { Ok(_) => {} - Err(error) => match ClientError::from(error) { + Err(error) => match error { Error::Call(rpc) => { print_error_rpc_response(rpc.code(), rpc.message().to_string(), cli.format); } @@ -634,7 +632,7 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client let result = cli.client.wallet_export(&cli.wallet).await?; let content = serde_json::to_string_pretty(&result).expect("result"); fs::write(path, content).map_err(|e| { - ClientError::Custom(format!("Could not save to path: {}", e.to_string())) + ClientError::Custom(format!("Could not save to path: {}", e)) })?; } Commands::GetWalletInfo => { @@ -698,12 +696,16 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client .await? } Commands::Renew { spaces, fee_rate } => { - let spaces: Vec<_> = spaces.into_iter().map(|s| { - let normalized = normalize_space(&s); - Subject::Label(SLabel::from_str(&normalized).expect("valid space")) - }).collect(); + let spaces: Vec<_> = spaces + .into_iter() + .map(|s| { + let normalized = normalize_space(&s); + Subject::Label(SLabel::from_str(&normalized).expect("valid space")) + }) + .collect(); cli.send_request( - Some(RpcWalletRequest::Transfer(TransferSpacesParams { secret: None, + Some(RpcWalletRequest::Transfer(TransferSpacesParams { + secret: None, spaces, to: None, data: None, @@ -722,8 +724,9 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client } => { let secret = if secret_stdin { let mut input = String::new(); - io::stdin().read_line(&mut input) - .map_err(|e| ClientError::Custom(format!("failed to read secret from stdin: {}", e)))?; + io::stdin().read_line(&mut input).map_err(|e| { + ClientError::Custom(format!("failed to read secret from stdin: {}", e)) + })?; Some(input.trim().to_string()) } else { None @@ -768,13 +771,17 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client use base64::Engine; let data = if let Some(raw_b64) = raw { // Raw base64-encoded wire-format bytes - base64::engine::general_purpose::STANDARD.decode(&raw_b64) - .map_err(|e| ClientError::Custom(format!("Could not base64 decode data: {}", e)))? + base64::engine::general_purpose::STANDARD + .decode(&raw_b64) + .map_err(|e| { + ClientError::Custom(format!("Could not base64 decode data: {}", e)) + })? } else if stdin { // Read JSON records from stdin let mut input = String::new(); - io::stdin().read_line(&mut input).map_err(|e| - ClientError::Custom(format!("Failed to read stdin: {}", e)))?; + io::stdin() + .read_line(&mut input) + .map_err(|e| ClientError::Custom(format!("Failed to read stdin: {}", e)))?; let record_set: sip7::RecordSet = serde_json::from_str(input.trim()) .map_err(|e| ClientError::Custom(format!("Invalid SIP-7 JSON: {}", e)))?; record_set.to_bytes() @@ -782,15 +789,29 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client // Build from --txt and --blob flags let mut records = Vec::new(); for txt in &txt_records { - let (key, value) = txt.split_once('=').ok_or_else(|| - ClientError::Custom(format!("Invalid --txt format '{}': expected key=value", txt)))?; + let (key, value) = txt.split_once('=').ok_or_else(|| { + ClientError::Custom(format!( + "Invalid --txt format '{}': expected key=value", + txt + )) + })?; records.push(sip7::Record::txt(key, &[value])); } for blob in &blob_records { - let (key, b64_value) = blob.split_once('=').ok_or_else(|| - ClientError::Custom(format!("Invalid --blob format '{}': expected key=base64", blob)))?; - let value = base64::engine::general_purpose::STANDARD.decode(b64_value) - .map_err(|e| ClientError::Custom(format!("Invalid base64 in --blob '{}': {}", key, e)))?; + let (key, b64_value) = blob.split_once('=').ok_or_else(|| { + ClientError::Custom(format!( + "Invalid --blob format '{}': expected key=base64", + blob + )) + })?; + let value = base64::engine::general_purpose::STANDARD + .decode(b64_value) + .map_err(|e| { + ClientError::Custom(format!( + "Invalid base64 in --blob '{}': {}", + key, e + )) + })?; records.push(sip7::Record::blob(key, value)); } sip7::RecordSet::pack(records) @@ -798,21 +819,22 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client .to_bytes() } else { return Err(ClientError::Custom( - "No data specified. Use --txt, --blob, --raw, or --stdin".to_string() + "No data specified. Use --txt, --blob, --raw, or --stdin".to_string(), )); }; cli.send_request( - Some(RpcWalletRequest::SetFallback(SetFallbackParams { subject, data })), + Some(RpcWalletRequest::SetFallback(SetFallbackParams { + subject, + data, + })), None, fee_rate, false, ) .await?; } - Commands::GetFallback { - subject, - } => { + Commands::GetFallback { subject } => { let response = cli.client.get_fallback(subject).await?; println!("{}", serde_json::to_string_pretty(&response)?); } @@ -944,10 +966,10 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client println!("{} Listing verified", "✓".color(Color::Green)); } Commands::GenerateKey => { - use spaces_wallet::bitcoin::secp256k1::{Secp256k1, Keypair}; use spaces_wallet::bitcoin::key::TapTweak; - use spaces_wallet::bitcoin::script::Builder; use spaces_wallet::bitcoin::opcodes::all::OP_PUSHNUM_1; + use spaces_wallet::bitcoin::script::Builder; + use spaces_wallet::bitcoin::secp256k1::{Keypair, Secp256k1}; let secp = Secp256k1::new(); let (secret_key, _) = secp.generate_keypair(&mut rand::thread_rng()); @@ -970,8 +992,10 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client Commands::CreateNum { bind_spk, fee_rate } => { let spk = match bind_spk { Some(hex) => { - let spk = ScriptBuf::from(hex::decode(hex) - .map_err(|_| ClientError::Custom("Invalid spk hex".to_string()))?); + let spk = ScriptBuf::from( + hex::decode(hex) + .map_err(|_| ClientError::Custom("Invalid spk hex".to_string()))?, + ); let num_id = NumId::from_spk::(spk.clone()); println!("Creating num id: {}", num_id); Some(spk) @@ -989,7 +1013,7 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client fee_rate, false, ) - .await? + .await? } Commands::GetNum { subject } => { let num = cli @@ -1010,17 +1034,19 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client } Commands::Operate { subject, fee_rate } => { cli.send_request( - Some(RpcWalletRequest::Operate(OperateParams { - subject, - })), + Some(RpcWalletRequest::Operate(OperateParams { subject })), None, fee_rate, false, ) - .await?; + .await?; println!("Operate setup should be complete once tx is confirmed"); } - Commands::Commit { subject, root, fee_rate } => { + Commands::Commit { + subject, + root, + fee_rate, + } => { cli.send_request( Some(RpcWalletRequest::Commit(CommitParams { subject, @@ -1030,7 +1056,7 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client fee_rate, false, ) - .await?; + .await?; } Commands::Rollback { subject, fee_rate } => { cli.send_request( @@ -1042,20 +1068,21 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client fee_rate, false, ) - .await?; + .await?; println!("Rollback transaction sent"); } - Commands::Delegate { subject, to, fee_rate } => { + Commands::Delegate { + subject, + to, + fee_rate, + } => { cli.send_request( - Some(RpcWalletRequest::Delegate(DelegateParams { - subject, - to, - })), + Some(RpcWalletRequest::Delegate(DelegateParams { subject, to })), None, fee_rate, false, ) - .await?; + .await?; } Commands::GetDelegator { subject } => { let delegator = cli diff --git a/client/src/bin/spaced.rs b/client/src/bin/spaced.rs index 319e1ed..87850b4 100644 --- a/client/src/bin/spaced.rs +++ b/client/src/bin/spaced.rs @@ -1,14 +1,10 @@ -use std::{env}; +use std::env; use env_logger::Env; use log::error; -use spaces_client::{ - config::{safe_exit}, -}; -use tokio::{ - sync::{broadcast}, -}; use spaces_client::app::App; +use spaces_client::config::safe_exit; +use tokio::sync::broadcast; #[tokio::main] async fn main() { @@ -26,7 +22,7 @@ async fn main() { match app.run(env::args().collect()).await { Ok(_) => {} Err(e) => { - error!("{}", e.to_string()); + error!("{}", e); safe_exit(1); } } diff --git a/client/src/cbf.rs b/client/src/cbf.rs index 2e428f0..f61989e 100644 --- a/client/src/cbf.rs +++ b/client/src/cbf.rs @@ -1,19 +1,21 @@ -use std::collections::{BTreeMap, HashSet, VecDeque}; -use std::time::Duration; +use crate::client::{BlockSource, BlockchainInfo}; +use crate::source::BitcoinBlockSource; +use crate::wallets::{WalletProgressUpdate, WalletStatus}; use anyhow::anyhow; use log::info; -use tokio::time::Instant; use spaces_protocol::bitcoin::BlockHash; use spaces_protocol::constants::ChainAnchor; -use spaces_wallet::bdk_wallet::chain::{local_chain, BlockId, ConfirmationBlockTime, IndexedTxGraph, TxUpdate}; +use spaces_wallet::SpacesWallet; use spaces_wallet::bdk_wallet::chain::keychain_txout::KeychainTxOutIndex; +use spaces_wallet::bdk_wallet::chain::{ + BlockId, ConfirmationBlockTime, IndexedTxGraph, TxUpdate, local_chain, +}; use spaces_wallet::bdk_wallet::{KeychainKind, Update}; -use spaces_wallet::bitcoin::bip158::BlockFilter; use spaces_wallet::bitcoin::ScriptBuf; -use spaces_wallet::SpacesWallet; -use crate::client::{BlockSource, BlockchainInfo}; -use crate::source::BitcoinBlockSource; -use crate::wallets::{WalletStatus, WalletProgressUpdate}; +use spaces_wallet::bitcoin::bip158::BlockFilter; +use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::time::Duration; +use tokio::time::Instant; pub struct CompactFilterSync { graph: IndexedTxGraph>, @@ -47,7 +49,10 @@ impl CompactFilterSync { pub fn new(wallet: &SpacesWallet) -> Self { let initial_tip = { let tip = wallet.local_chain().tip(); - ChainAnchor { height: tip.height(), hash: tip.hash() } + ChainAnchor { + height: tip.height(), + hash: tip.hash(), + } }; let mut cbf = Self { @@ -110,7 +115,10 @@ impl CompactFilterSync { source: &BitcoinBlockSource, progress: &mut WalletProgressUpdate, ) -> anyhow::Result<()> { - if self.wait.is_some_and(|w| w.elapsed() < Duration::from_secs(10)) { + if self + .wait + .is_some_and(|w| w.elapsed() < Duration::from_secs(10)) + { return Ok(()); } self.wait = None; @@ -121,7 +129,12 @@ impl CompactFilterSync { // if wallet already past prune height, we don't need filters if let Some(prune_height) = info.prune_height { if self.initial_tip.height >= prune_height { - info!("wallet({}): tip {} >= prune height {}, cbf done", wallet.name(), self.initial_tip.height, prune_height); + info!( + "wallet({}): tip {} >= prune height {}, cbf done", + wallet.name(), + self.initial_tip.height, + prune_height + ); self.state = SyncState::Synced; return Ok(()); } @@ -140,8 +153,9 @@ impl CompactFilterSync { } info!("Filters syncing, retrying..."); - *progress = WalletProgressUpdate::new(WalletStatus::CbfFilterSync, - Some(info.filters_progress.unwrap_or(0.0)) + *progress = WalletProgressUpdate::new( + WalletStatus::CbfFilterSync, + Some(info.filters_progress.unwrap_or(0.0)), ); self.wait = Some(Instant::now()); return Ok(()); @@ -154,17 +168,21 @@ impl CompactFilterSync { .ok_or_else(|| anyhow!("filter sync: checkpoint missing"))?; if self.initial_tip.height < checkpoint.height { return Err(anyhow!( - "Wallet birthday {} < checkpoint {}", self.initial_tip.height, checkpoint.height + "Wallet birthday {} < checkpoint {}", + self.initial_tip.height, + checkpoint.height )); } let start = self.initial_tip.height; - let end = info - .prune_height - .ok_or(anyhow!("Prune height missing"))?; + let end = info.prune_height.ok_or(anyhow!("Prune height missing"))?; let available_filters = info.filters.ok_or(anyhow!("Filters missing"))?; if end > available_filters { - return Err(anyhow!("Prune height {} > {} available filters", end, available_filters)); + return Err(anyhow!( + "Prune height {} > {} available filters", + end, + available_filters + )); } if start >= end { @@ -193,21 +211,33 @@ impl CompactFilterSync { self.queued_blocks.insert(height, idx_filter.hash); self.load_more_scripts(wallet); self.block_matches += 1; - info!("wallet({}) processed block filter {} - match found", wallet.name(), height); + info!( + "wallet({}) processed block filter {} - match found", + wallet.name(), + height + ); } else { - info!("wallet({}) processed block filter {} - no match", wallet.name(), height); + info!( + "wallet({}) processed block filter {} - no match", + wallet.name(), + height + ); } let completed = self.total_filters as f32 - self.queued_filters.len() as f32; *progress = WalletProgressUpdate::new( WalletStatus::CbfProcessFilters, - Some(completed / self.total_filters as f32) + Some(completed / self.total_filters as f32), ); } SyncState::QueueBlocks => { if !self.queued_blocks.is_empty() { let heights: Vec = self.queued_blocks.keys().copied().collect(); - info!("wallet({}): queueing {} blocks", wallet.name(), heights.len()); + info!( + "wallet({}): queueing {} blocks", + wallet.name(), + heights.len() + ); source.queue_blocks(heights)?; } self.state = SyncState::WaitForBlocks; @@ -220,7 +250,11 @@ impl CompactFilterSync { .ok_or_else(|| anyhow!("filter sync: block queue missing"))?; if status.pending > 0 { - info!("wallet({}): waiting for {} pending blocks", wallet.name(), status.pending); + info!( + "wallet({}): waiting for {} pending blocks", + wallet.name(), + status.pending + ); // The client has a global state for pending blocks in the queue // so we cap it just in case other things are queuing blocks @@ -229,7 +263,7 @@ impl CompactFilterSync { let completed = self.block_matches as f32 - pending; *progress = WalletProgressUpdate::new( WalletStatus::CbfDownloadMatchingBlocks, - Some(completed / self.block_matches as f32) + Some(completed / self.block_matches as f32), ); self.wait = Some(Instant::now()); return Ok(()); @@ -237,7 +271,9 @@ impl CompactFilterSync { if status.completed < self.queued_blocks.len() as u32 { return Err(anyhow!( - "incomplete downloads: {} of {}", status.completed, self.queued_blocks.len() + "incomplete downloads: {} of {}", + status.completed, + self.queued_blocks.len() )); } self.state = SyncState::ProcessBlocks; @@ -251,19 +287,31 @@ impl CompactFilterSync { } Some(f) => f, }; - info!("wallet({}): processing block {} {}", wallet.name(), height, hash); - let block = source.get_block(&hash)? - .ok_or(anyhow!("block {} {} not found", height, hash))?; + info!( + "wallet({}): processing block {} {}", + wallet.name(), + height, + hash + ); + let block = source.get_block(&hash)?.ok_or(anyhow!( + "block {} {} not found", + height, + hash + ))?; self.chain_changeset.insert(height, Some(hash)); let _ = self.graph.apply_block_relevant(&block, height); let completed = self.block_matches - self.queued_blocks.len() as u32; *progress = WalletProgressUpdate::new( WalletStatus::CbfProcessMatchingBlocks, - Some(completed as f32 / self.block_matches as f32) + Some(completed as f32 / self.block_matches as f32), ); } SyncState::ApplyUpdate => { - info!("wallet({}): updating wallet tip to {}", wallet.name(), self.filters_tip); + info!( + "wallet({}): updating wallet tip to {}", + wallet.name(), + self.filters_tip + ); let filters_anchor = BlockId { height: self.filters_tip, hash: source.get_block_hash(self.filters_tip)?, @@ -272,7 +320,11 @@ impl CompactFilterSync { let update = self.get_scan_response(); wallet.apply_update(update)?; wallet.insert_checkpoint(filters_anchor)?; - info!("wallet({}): compact filter sync portion complete at {}", wallet.name(), self.filters_tip); + info!( + "wallet({}): compact filter sync portion complete at {}", + wallet.name(), + self.filters_tip + ); self.state = SyncState::Synced; // Only CBF portion is done *progress = WalletProgressUpdate::new(WalletStatus::Syncing, None); diff --git a/client/src/checker.rs b/client/src/checker.rs index 9009b40..691ea80 100644 --- a/client/src/checker.rs +++ b/client/src/checker.rs @@ -2,15 +2,15 @@ use std::collections::BTreeMap; use anyhow::anyhow; use spaces_protocol::{ + Covenant, RevokeReason, SpaceOut, bitcoin::{OutPoint, Transaction}, hasher::{KeyHasher, SpaceKey}, prepare::{SpacesSource, TxContext}, validate::{TxChangeSet, UpdateKind, Validator}, - Covenant, RevokeReason, SpaceOut, }; -use crate::store::chain::Chain; use crate::store::Sha256; +use crate::store::chain::Chain; pub struct TxChecker<'a> { pub original: &'a mut Chain, @@ -46,7 +46,7 @@ impl<'a> TxChecker<'a> { ) -> anyhow::Result> { let changeset = self.apply_tx(height, tx)?; if let Some(changeset) = changeset.as_ref() { - Self::check(&changeset)?; + Self::check(changeset)?; } Ok(changeset) } @@ -115,29 +115,27 @@ impl<'a> TxChecker<'a> { .iter() .any(|spend| spend.script_error.is_some()) { - return Err(anyhow!("tx-check: transaction not broadcasted as it may have an open that will be rejected")); + return Err(anyhow!( + "tx-check: transaction not broadcasted as it may have an open that will be rejected" + )); } for create in changset.creates.iter() { if let Some(space) = create.space.as_ref() { - match space.covenant { - Covenant::Reserved => { - return Err(anyhow!("tx-check: transaction not broadcasted as it may cause spaces to use a reserved covenant")) - } - _ => {} + if matches!(space.covenant, Covenant::Reserved) { + return Err(anyhow!( + "tx-check: transaction not broadcasted as it may cause spaces to use a reserved covenant" + )); } } } for update in changset.updates.iter() { - match update.kind { - UpdateKind::Revoke(kind) => { - match kind { - RevokeReason::Expired => {} - _ => { - return Err(anyhow!("tx-check: transaction not broadcasted as it may cause a space to be revoked (code: {:?})", kind)) - } - } - } - _ => {} + if let UpdateKind::Revoke(kind) = update.kind + && !matches!(kind, RevokeReason::Expired) + { + return Err(anyhow!( + "tx-check: transaction not broadcasted as it may cause a space to be revoked (code: {:?})", + kind + )); } } Ok(()) @@ -150,8 +148,8 @@ impl SpacesSource for TxChecker<'_> { space_hash: &SpaceKey, ) -> spaces_protocol::errors::Result> { match self.spaces.get(space_hash) { - None => self.original.get_space_outpoint(space_hash.into()), - Some(res) => Ok(res.clone()), + None => self.original.get_space_outpoint(space_hash), + Some(res) => Ok(*res), } } diff --git a/client/src/client.rs b/client/src/client.rs index e97d011..154eef6 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -3,27 +3,25 @@ pub extern crate spaces_protocol; use std::{error::Error, fmt}; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::Error as SerdeError; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use spaces_nums::{CommitmentKey, CommitmentTipKey, DelegatorKey, NumOutpointKey}; use spaces_protocol::{ + Bytes, Covenant, FullSpaceOut, RevokeReason, SpaceOut, bitcoin::{Amount, Block, BlockHash, OutPoint, Txid}, constants::{ChainAnchor, ROLLOUT_BATCH_SIZE, ROLLOUT_BLOCK_INTERVAL}, hasher::{BidKey, KeyHasher, OutpointKey, SpaceKey}, prepare::TxContext, validate::{TxChangeSet, UpdateKind, Validator}, - Bytes, Covenant, FullSpaceOut, RevokeReason, SpaceOut, }; -use spaces_nums::{CommitmentKey, CommitmentTipKey, DelegatorKey, NumOutpointKey}; use spaces_wallet::bitcoin::{Network, Transaction}; -use crate::{ - source::{BitcoinRpcError, BestChain}, -}; use crate::source::BlockQueueResult; -use crate::store::chain::{Chain}; +use crate::source::{BestChain, BitcoinRpcError}; use crate::store::Sha256; +use crate::store::chain::Chain; pub trait BlockSource { fn get_block_hash(&self, height: u32) -> Result; @@ -31,9 +29,16 @@ pub trait BlockSource { fn get_median_time(&self) -> Result; fn in_mempool(&self, txid: &Txid, height: u32) -> Result; fn get_block_count(&self) -> Result; - fn get_best_chain(&self, tip: Option, expected_chain: Network) -> Result; + fn get_best_chain( + &self, + tip: Option, + expected_chain: Network, + ) -> Result; fn get_blockchain_info(&self) -> Result; - fn get_block_filter_by_height(&self, height: u32) -> Result, BitcoinRpcError>; + fn get_block_filter_by_height( + &self, + height: u32, + ) -> Result, BitcoinRpcError>; fn queue_blocks(&self, heights: Vec) -> Result<(), BitcoinRpcError>; fn queue_filters(&self) -> Result<(), BitcoinRpcError>; } @@ -42,10 +47,7 @@ pub trait BlockSource { pub struct BlockFilterRpc { pub hash: BlockHash, pub height: u32, - #[serde( - serialize_with = "serialize_hex", - deserialize_with = "deserialize_hex" - )] + #[serde(serialize_with = "serialize_hex", deserialize_with = "deserialize_hex")] pub content: Vec, } @@ -74,7 +76,6 @@ pub struct BlockchainInfo { pub headers_synced: Option, } - #[derive(Debug, Clone)] pub struct Client { validator: Validator, @@ -105,7 +106,6 @@ pub struct PtrTxEntry { pub tx: Option, } - #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct TxEntry { #[serde(flatten)] @@ -147,16 +147,21 @@ impl Client { } } - fn verify_block_connected(chain: &mut Chain, height: u32, block_hash: BlockHash, block: &Block) -> anyhow::Result<()> { + fn verify_block_connected( + chain: &mut Chain, + height: u32, + block_hash: BlockHash, + block: &Block, + ) -> anyhow::Result<()> { // Spaces tip must connect to block { let tip = chain.tip(); if tip.hash != block.header.prev_blockhash || tip.height + 1 != height { return Err(SyncError { - checkpoint: tip.clone(), + checkpoint: tip, connect_to: (height, block_hash), } - .into()); + .into()); } } // Nums tip must connect to block @@ -164,10 +169,10 @@ impl Client { let tip = chain.nums_tip(); if tip.hash != block.header.prev_blockhash || tip.height + 1 != height { return Err(SyncError { - checkpoint: tip.clone(), + checkpoint: tip, connect_to: (height, block_hash), } - .into()); + .into()); } } @@ -232,7 +237,7 @@ impl Client { let mut spaceouts_input_ctx = None; if let Some(prepared) = TxContext::from_tx::(chain, tx)? { spaceouts_input_ctx = Some(prepared.inputs.clone()); - let validated_tx = self.validator.process(height, &tx, prepared); + let validated_tx = self.validator.process(height, tx, prepared); spaceouts = Some(validated_tx.creates.clone()); if let Some(idx) = spaces_meta.as_mut() { @@ -250,7 +255,7 @@ impl Client { }, }); } - self.apply_space_tx(chain, &tx, validated_tx); + self.apply_space_tx(chain, tx, validated_tx); } let ptrs_ctx = if chain.can_scan_nums(height) { @@ -258,17 +263,28 @@ impl Client { chain, tx, spaceouts_input_ctx.is_some(), - spaceouts.clone().unwrap_or(vec![]) , height)? + spaceouts.clone().unwrap_or(vec![]), + height, + )? } else { None }; if let Some(ptrs_ctx) = ptrs_ctx { - let spent_spaceouts = spaceouts_input_ctx.unwrap_or_default().into_iter() - .map(|input| input.sstxo.previous_output).collect::>(); + let spent_spaceouts = spaceouts_input_ctx + .unwrap_or_default() + .into_iter() + .map(|input| input.sstxo.previous_output) + .collect::>(); let created_spaceouts = spaceouts.unwrap_or_default(); - let ptrs_validated = self.ptr_validator - .process::(height, &tx, position as _, ptrs_ctx, spent_spaceouts, created_spaceouts); + let ptrs_validated = self.ptr_validator.process::( + height, + tx, + position as _, + ptrs_ctx, + spent_spaceouts, + created_spaceouts, + ); if let Some(idx) = num_meta.as_mut() { { @@ -299,7 +315,12 @@ impl Client { Ok((spaces_meta, num_meta)) } - fn apply_ptrs_tx(&self, state: &mut Chain, tx: &Transaction, changeset: spaces_nums::TxChangeSet) { + fn apply_ptrs_tx( + &self, + state: &mut Chain, + tx: &Transaction, + changeset: spaces_nums::TxChangeSet, + ) { // Remove spends for n in changeset.spends.into_iter() { let previous = tx.input[n].previous_output; @@ -313,7 +334,8 @@ impl Client { } // Remove revoked commitments for revoked in changeset.revoked_commitments { - let commitment_key = CommitmentKey::new::(&revoked.space, revoked.commitment.state_root); + let commitment_key = + CommitmentKey::new::(&revoked.space, revoked.commitment.state_root); state.remove_commitment(commitment_key); let registry_key = CommitmentTipKey::from_slabel::(&revoked.space); @@ -333,14 +355,16 @@ impl Client { // Insert new commitments for commitment_info in changeset.commitments { - let commitment_key = CommitmentKey::new::(&commitment_info.space, commitment_info.commitment.state_root); + let commitment_key = CommitmentKey::new::( + &commitment_info.space, + commitment_info.commitment.state_root, + ); let registry_key = CommitmentTipKey::from_slabel::(&commitment_info.space); // Points space -> commitments tip state.insert_commitment_tip(registry_key, commitment_info.commitment.state_root); // commitment key = HASH(HASH(space) || state root) -> commitment state.insert_commitment(commitment_key, commitment_info.commitment); - } // Create ptrs @@ -385,18 +409,15 @@ impl Client { state.remove_space(space_key); // Remove any bids from pre-auction pool - match space.covenant { - Covenant::Bid { - total_burned, - claim_height, - .. - } => { - if claim_height.is_none() { - let bid_key = BidKey::from_bid(total_burned, base_hash); - state.remove_bid(bid_key) - } - } - _ => {} + if let Covenant::Bid { + total_burned, + claim_height, + .. + } = space.covenant + && claim_height.is_none() + { + let bid_key = BidKey::from_bid(total_burned, base_hash); + state.remove_bid(bid_key) } } RevokeReason::Expired => { diff --git a/client/src/config.rs b/client/src/config.rs index f336ee5..cda8967 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -12,17 +12,17 @@ use jsonrpsee::core::Serialize; use log::error; use rand::{ distributions::Alphanumeric, - {thread_rng, Rng}, + {Rng, thread_rng}, }; use serde::Deserialize; use spaces_protocol::bitcoin::Network; +use crate::store::chain::{Chain, ROOT_ANCHORS_COUNT}; use crate::{ auth::{auth_token_from_cookie, auth_token_from_creds}, source::{BitcoinRpc, BitcoinRpcAuth}, spaces::Spaced, }; -use crate::store::chain::{Chain, ROOT_ANCHORS_COUNT}; const RPC_OPTIONS: &str = "RPC Server Options"; @@ -72,7 +72,7 @@ pub struct Args { /// This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost) #[arg(long, help_heading = Some(RPC_OPTIONS), default_values = ["127.0.0.1", "::1"], env = "SPACED_RPC_BIND")] rpc_bind: Vec, - /// Listen for JSON-RPC connections on + /// Listen for JSON-RPC connections on `` #[arg(long, help_heading = Some(RPC_OPTIONS), env = "SPACED_RPC_PORT")] rpc_port: Option, /// Index blocks including the full transaction data @@ -126,6 +126,7 @@ impl ExtendedNetwork { } } + #[allow(clippy::result_unit_err)] pub fn from_core_arg(arg: &str) -> Result { match arg.to_lowercase().as_str() { "main" => Ok(ExtendedNetwork::Mainnet), @@ -200,13 +201,17 @@ impl Args { let bitcoin_rpc_auth = if let Some(cookie) = args.bitcoin_rpc_cookie { let cookie = std::fs::read_to_string(&cookie).map_err(|e| { - anyhow!("Failed to read Bitcoin RPC cookie '{}': {}", cookie.display(), e) + anyhow!( + "Failed to read Bitcoin RPC cookie '{}': {}", + cookie.display(), + e + ) })?; BitcoinRpcAuth::Cookie(cookie) } else if let Some(user) = args.bitcoin_rpc_user { BitcoinRpcAuth::UserPass(user, args.bitcoin_rpc_password.expect("password")) - } else if let Some(cookie) = default_bitcoin_cookie_path(&args.chain) - .and_then(|p| std::fs::read_to_string(&p).ok()) + } else if let Some(cookie) = + default_bitcoin_cookie_path(&args.chain).and_then(|p| std::fs::read_to_string(&p).ok()) { log::info!("Using Bitcoin Core cookie authentication"); BitcoinRpcAuth::Cookie(cookie) @@ -288,7 +293,9 @@ pub fn default_bitcoin_cookie_path(network: &ExtendedNetwork) -> Option } else if cfg!(target_os = "macos") { home?.join("Library/Application Support/Bitcoin") } else if cfg!(target_os = "windows") { - std::env::var_os("APPDATA").map(PathBuf::from)?.join("Bitcoin") + std::env::var_os("APPDATA") + .map(PathBuf::from)? + .join("Bitcoin") } else { return None; }; diff --git a/client/src/format.rs b/client/src/format.rs index 8297e5d..a6a0805 100644 --- a/client/src/format.rs +++ b/client/src/format.rs @@ -3,9 +3,11 @@ use colored::{Color, Colorize}; use jsonrpsee::core::Serialize; use serde::Deserialize; use spaces_protocol::{ - bitcoin::{Amount, Network, OutPoint}, Covenant + Covenant, + bitcoin::{Amount, Network, OutPoint}, }; use spaces_wallet::{ + Balance, DoubleUtxo, WalletOutput, address::SpaceAddress, bdk_wallet::KeychainKind, bitcoin::{Address, Txid}, @@ -13,15 +15,14 @@ use spaces_wallet::{ BidEventDetails, BidoutEventDetails, OpenEventDetails, SendEventDetails, TransferEventDetails, TxEventKind, }, - Balance, DoubleUtxo, WalletOutput, }; use tabled::{Table, Tabled}; +use crate::wallets::{WalletInfoWithProgress, WalletStatus}; use crate::{ rpc::ServerInfo, wallets::{ListNumsResponse, ListSpacesResponse, TxInfo, TxResponse, WalletResponse}, }; -use crate::wallets::{WalletInfoWithProgress, WalletStatus}; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, ValueEnum, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -184,7 +185,10 @@ pub fn print_wallet_info(prog: WalletInfoWithProgress, format: Format) { match format { Format::Text => { println!("WALLET: {}", prog.info.label); - println!(" Tip {}\n Birthday {}", prog.info.tip, prog.info.start_block); + println!( + " Tip {}\n Birthday {}", + prog.info.tip, prog.info.start_block + ); println!(" Public descriptors"); for desc in prog.info.descriptors { @@ -195,7 +199,7 @@ pub fn print_wallet_info(prog: WalletInfoWithProgress, format: Format) { println!(" Sync Status:"); let p = prog.sync.progress.unwrap_or(0.0); match prog.sync.status { - WalletStatus::HeadersSync => { + WalletStatus::HeadersSync => { println!(" Syncing block headers"); } WalletStatus::ChainSync => { @@ -204,20 +208,23 @@ pub fn print_wallet_info(prog: WalletInfoWithProgress, format: Format) { WalletStatus::SpacesSync => { println!(" Spaces Syncing: {:.1}%", p * 100.0); } - WalletStatus::CbfFilterSync => { + WalletStatus::CbfFilterSync => { println!(" Filters Syncing: {:.1}%", p * 100.0); } WalletStatus::CbfProcessFilters => { - println!(" Processing Filters: {:.1}%", p* 100.0); + println!(" Processing Filters: {:.1}%", p * 100.0); } WalletStatus::CbfDownloadMatchingBlocks => { println!(" Downloading Matching Blocks: {:.1}%", p * 100.0); } WalletStatus::CbfProcessMatchingBlocks => { - println!(" Processing Matching Blocks: {:.1}%",p * 100.0); + println!(" Processing Matching Blocks: {:.1}%", p * 100.0); } WalletStatus::Syncing => { - println!(" Syncing: In progress ({:.1}%):", prog.info.progress * 100.0); + println!( + " Syncing: In progress ({:.1}%):", + prog.info.progress * 100.0 + ); } WalletStatus::CbfApplyUpdate => { println!(" Applying compact filters update"); @@ -225,7 +232,6 @@ pub fn print_wallet_info(prog: WalletInfoWithProgress, format: Format) { WalletStatus::Complete => { println!(" Complete"); } - } println!(); @@ -238,7 +244,7 @@ pub fn print_wallet_info(prog: WalletInfoWithProgress, format: Format) { fn ascii_table(iter: I) -> String where - I: IntoIterator, + I: IntoIterator, T: Tabled, { Table::new(iter) @@ -282,7 +288,9 @@ pub fn print_list_spaces_response( let mut winnings = Vec::new(); let mut owned = Vec::new(); for slabel in response.pending { - pendings.push(PendingSpaces { space: slabel.to_string() }); + pendings.push(PendingSpaces { + space: slabel.to_string(), + }); } for res in response.outbid { let space = res.spaceout.space.as_ref().expect("space"); @@ -291,16 +299,14 @@ pub fn print_list_spaces_response( last_confirmed_bid: 0, days_left: "".to_string(), }; - match space.covenant { - Covenant::Bid { - total_burned, - claim_height, - .. - } => { - outbid.last_confirmed_bid = total_burned.to_sat(); - outbid.days_left = format_days_left(current_block, claim_height); - } - _ => {} + if let Covenant::Bid { + total_burned, + claim_height, + .. + } = space.covenant + { + outbid.last_confirmed_bid = total_burned.to_sat(); + outbid.days_left = format_days_left(current_block, claim_height); } outbids.push(outbid); } @@ -313,22 +319,20 @@ pub fn print_list_spaces_response( days_left: "--".to_string(), claim_at: "--".to_string(), }; - match space.covenant { - Covenant::Bid { - total_burned, - claim_height, - .. - } => { - winning.bid = total_burned.to_sat(); - winning.claim_at = claim_height - .map(|h| h.to_string()) - .unwrap_or("--".to_string()); - winning.days_left = format_days_left(current_block, claim_height); - if winning.days_left == "0.00" { - winning.days_left = "Ready to claim".to_string(); - } + if let Covenant::Bid { + total_burned, + claim_height, + .. + } = space.covenant + { + winning.bid = total_burned.to_sat(); + winning.claim_at = claim_height + .map(|h| h.to_string()) + .unwrap_or("--".to_string()); + winning.days_left = format_days_left(current_block, claim_height); + if winning.days_left == "0.00" { + winning.days_left = "Ready to claim".to_string(); } - _ => {} } winnings.push(winning); } @@ -340,19 +344,18 @@ pub fn print_list_spaces_response( days_left: "--".to_string(), utxo: res.outpoint(), }; - match &space.covenant { - Covenant::Transfer { expire_height, .. } => { - registered.expire_at = *expire_height as _; - registered.days_left = - format_days_left(current_block, Some(*expire_height)); - } - _ => {} + if let Covenant::Transfer { expire_height, .. } = &space.covenant { + registered.expire_at = *expire_height as _; + registered.days_left = format_days_left(current_block, Some(*expire_height)); } owned.push(registered); } if !pendings.is_empty() { - println!("⏳ PENDING ({} spaces): ", pendings.len().to_string().bold()); + println!( + "⏳ PENDING ({} spaces): ", + pendings.len().to_string().bold() + ); let table = ascii_table(pendings); println!("{}", table); } @@ -374,11 +377,7 @@ pub fn print_list_spaces_response( } if !owned.is_empty() { - println!( - "{} ({} spaces): ", - "🔑 OWNED", - owned.len().to_string().bold() - ); + println!("🔑 OWNED ({} spaces): ", owned.len().to_string().bold()); let table = ascii_table(owned); println!("{}", table); } @@ -411,15 +410,17 @@ pub fn print_wallet_response_text(network: Network, response: WalletResponse) { let mut secondary_txs = Vec::new(); for tx in response.result { - if tx.events.iter().any(|event| match event.kind { - TxEventKind::Open - | TxEventKind::Bid - | TxEventKind::Register - | TxEventKind::Transfer - | TxEventKind::Send - | TxEventKind::Renew - | TxEventKind::Buy => true, - _ => false, + if tx.events.iter().any(|event| { + matches!( + event.kind, + TxEventKind::Open + | TxEventKind::Bid + | TxEventKind::Register + | TxEventKind::Transfer + | TxEventKind::Send + | TxEventKind::Renew + | TxEventKind::Buy + ) }) { main_txs.push(tx); } else { diff --git a/client/src/lib.rs b/client/src/lib.rs index 934d626..64fb07a 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -9,7 +9,9 @@ use std::time::{Duration, Instant}; use base64::Engine; use serde::{Deserialize, Deserializer, Serializer}; +pub mod app; pub mod auth; +mod cbf; mod checker; pub mod client; pub mod config; @@ -18,11 +20,9 @@ pub mod rpc; #[cfg(feature = "schema")] pub mod rpc_schema; pub mod source; +mod spaces; pub mod store; pub mod wallets; -mod cbf; -pub mod app; -mod spaces; fn std_wait(mut predicate: F, wait: Duration) where diff --git a/client/src/rpc.rs b/client/src/rpc.rs index 3cb64fa..75546bf 100644 --- a/client/src/rpc.rs +++ b/client/src/rpc.rs @@ -1,77 +1,78 @@ -use std::{ - collections::BTreeMap, fs, fs::File, io::Write, net::SocketAddr, path::PathBuf, str::FromStr, - sync::Arc, +use crate::auth::BasicAuthLayer; +use crate::store::Sha256; +use crate::store::chain::{CACHED_SNAPSHOT_LOOKBACK, COMMIT_BLOCK_INTERVAL, Chain}; +use crate::store::spaces::RolloutEntry; +use crate::wallets::WalletInfoWithProgress; +use crate::{ + calc_progress, + checker::TxChecker, + client::{BlockMeta, BlockchainInfo, NumBlockMeta, TxEntry}, + config::ExtendedNetwork, + deserialize_base64, serialize_base64, + source::BitcoinRpc, + wallets::{ + AddressKind, ListNumsResponse, ListSpacesResponse, RpcWallet, TxInfo, TxResponse, + WalletCommand, WalletResponse, + }, }; -use std::collections::HashSet; -use anyhow::{anyhow, Context}; +use anyhow::{Context, anyhow}; use bdk::{ + KeychainKind, bitcoin::{Amount, BlockHash, FeeRate, Network, Txid}, chain::BlockId, keys::{ - bip39::{Language, Mnemonic, WordCount}, DerivableKey, ExtendedKey, GeneratableKey, GeneratedKey, + bip39::{Language, Mnemonic, WordCount}, }, miniscript::Tap, - KeychainKind, }; use jsonrpsee::{ core::async_trait, proc_macros::rpc, - server::{middleware::http::ProxyGetRequestLayer, Server}, + server::{Server, middleware::http::ProxyGetRequestLayer}, types::ErrorObjectOwned, }; use log::info; use serde::{Deserialize, Serialize}; use spacedb::tx::ProofType; +use spaces_nums::num_id::NumId; +use spaces_nums::snumeric::SNumeric; +use spaces_nums::{ + ChainProofRequest, Commitment, CommitmentKey, CommitmentTipKey, DelegatorKey, FullNumOut, + NumKeyKind, NumOut, NumOutpointKey, NumSource, RootAnchor, +}; +use spaces_protocol::bitcoin::ScriptBuf; +use spaces_protocol::hasher::Hash; use spaces_protocol::{ - bitcoin, + Bytes, Covenant, FullSpaceOut, SpaceOut, bitcoin, bitcoin::{ - bip32::Xpriv, Network::{Regtest, Testnet}, OutPoint, + bip32::Xpriv, }, constants::ChainAnchor, hasher::{KeyHasher, OutpointKey, SpaceKey}, prepare::SpacesSource, slabel::SLabel, validate::TxChangeSet, - Bytes, Covenant, FullSpaceOut, SpaceOut, }; +pub use spaces_wallet::Subject; +use spaces_wallet::bitcoin::hashes::sha256; use spaces_wallet::{ + Balance, DoubleUtxo, Listing, SpacesWallet, WalletConfig, WalletDescriptors, WalletOutput, bdk_wallet as bdk, bdk_wallet::template::Bip86, bitcoin::hashes::Hash as BitcoinHash, - bitcoin::secp256k1::schnorr, - export::WalletExport, Balance, DoubleUtxo, Listing, SpacesWallet, - WalletConfig, WalletDescriptors, WalletOutput, + bitcoin::secp256k1::schnorr, export::WalletExport, +}; +use std::collections::HashSet; +use std::{ + collections::BTreeMap, fs, fs::File, io::Write, net::SocketAddr, path::PathBuf, str::FromStr, + sync::Arc, }; -pub use spaces_wallet::Subject; use tokio::{ select, - sync::{broadcast, mpsc, oneshot, RwLock}, + sync::{RwLock, broadcast, mpsc, oneshot}, task::JoinSet, }; -use spaces_protocol::bitcoin::ScriptBuf; -use spaces_protocol::hasher::Hash; -use spaces_nums::{NumSource, FullNumOut, NumOut, Commitment, CommitmentTipKey, CommitmentKey, DelegatorKey, NumOutpointKey, RootAnchor, ChainProofRequest, NumKeyKind}; -use spaces_nums::snumeric::SNumeric; -use spaces_nums::num_id::NumId; -use spaces_wallet::bitcoin::hashes::sha256; -use crate::auth::BasicAuthLayer; -use crate::wallets::WalletInfoWithProgress; -use crate::{ - calc_progress, - checker::TxChecker, - client::{BlockMeta, NumBlockMeta, TxEntry, BlockchainInfo}, - config::ExtendedNetwork, - deserialize_base64, serialize_base64, - source::BitcoinRpc, - wallets::{ - AddressKind, ListNumsResponse, ListSpacesResponse, RpcWallet, TxInfo, TxResponse, - WalletCommand, WalletResponse, - }, -}; -use crate::store::chain::{Chain, COMMIT_BLOCK_INTERVAL, CACHED_SNAPSHOT_LOOKBACK}; -use crate::store::Sha256; -use crate::store::spaces::RolloutEntry; pub(crate) type Responder = oneshot::Sender; @@ -87,7 +88,6 @@ pub struct ServerInfo { pub progress: f32, } - #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct ChainInfo { @@ -219,7 +219,6 @@ pub struct AsyncChainState { sender: mpsc::Sender, } - #[rpc(server, client)] pub trait Rpc { #[method(name = "getserverinfo")] @@ -241,27 +240,24 @@ pub trait Rpc { async fn get_spaceout(&self, outpoint: OutPoint) -> Result, ErrorObjectOwned>; #[method(name = "getnum")] - async fn get_num( - &self, - subject: Subject, - ) -> Result, ErrorObjectOwned>; + async fn get_num(&self, subject: Subject) -> Result, ErrorObjectOwned>; #[method(name = "getnumowner")] - async fn get_num_owner( - &self, - subject: Subject, - ) -> Result, ErrorObjectOwned>; + async fn get_num_owner(&self, subject: Subject) -> Result, ErrorObjectOwned>; #[method(name = "getnumout")] async fn get_numout(&self, outpoint: OutPoint) -> Result, ErrorObjectOwned>; #[method(name = "getcommitment")] - async fn get_commitment(&self, subject: Subject, root: Option) -> Result, ErrorObjectOwned>; + async fn get_commitment( + &self, + subject: Subject, + root: Option, + ) -> Result, ErrorObjectOwned>; #[method(name = "getdelegation")] async fn get_delegation(&self, subject: Subject) -> Result, ErrorObjectOwned>; - #[method(name = "getdelegator")] async fn get_delegator(&self, subject: Subject) -> Result, ErrorObjectOwned>; @@ -301,7 +297,6 @@ pub trait Rpc { #[method(name = "walletimport")] async fn wallet_import(&self, wallet: WalletExport) -> Result<(), ErrorObjectOwned>; - #[method(name = "walletcanoperate")] async fn wallet_can_operate( &self, @@ -327,7 +322,7 @@ pub trait Rpc { #[method(name = "walletgetinfo")] async fn wallet_get_info(&self, name: &str) - -> Result; + -> Result; #[method(name = "walletexport")] async fn wallet_export(&self, name: &str) -> Result; @@ -447,7 +442,11 @@ pub trait Rpc { /// Debug method to set a space's expire height (regtest only) #[method(name = "debugsetexpireheight")] - async fn debug_set_expire_height(&self, space: &str, expire_height: u32) -> Result<(), ErrorObjectOwned>; + async fn debug_set_expire_height( + &self, + space: &str, + expire_height: u32, + ) -> Result<(), ErrorObjectOwned>; } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -604,7 +603,6 @@ pub struct RpcServerImpl { client: reqwest::Client, } - /// Combined proof result for a chain proof request containing subtrees from both /// spaces and ptrs trees at the same snapshot height. #[derive(Clone, Serialize, Deserialize)] @@ -679,7 +677,11 @@ impl WalletManager { Ok(export) } - pub async fn create_wallet(&self, client: &reqwest::Client, name: &str) -> anyhow::Result { + pub async fn create_wallet( + &self, + client: &reqwest::Client, + name: &str, + ) -> anyhow::Result { let mnemonic: GeneratedKey<_, Tap> = Mnemonic::generate((WordCount::Words12, Language::English)) .map_err(|_| anyhow!("Mnemonic generation error"))?; @@ -690,7 +692,12 @@ impl WalletManager { Ok(mnemonic.to_string()) } - pub async fn recover_wallet(&self, client: &reqwest::Client, name: &str, mnemonic: &str) -> anyhow::Result<()> { + pub async fn recover_wallet( + &self, + client: &reqwest::Client, + name: &str, + mnemonic: &str, + ) -> anyhow::Result<()> { let start_block = self.get_wallet_start_block(client).await?; self.setup_new_wallet(name.to_string(), mnemonic.to_string(), start_block)?; self.load_wallet(name).await?; @@ -836,13 +843,13 @@ impl WalletManager { async fn get_wallet_start_block(&self, client: &reqwest::Client) -> anyhow::Result { let count: i32 = self .rpc - .send_json(&client, &self.rpc.get_block_count()) + .send_json(client,&self.rpc.get_block_count()) .await?; let height = std::cmp::max(count - 1, 0) as u32; let hash = self .rpc - .send_json(&client, &self.rpc.get_block_hash(height)) + .send_json(client,&self.rpc.get_block_hash(height)) .await?; Ok(BlockId { height, hash }) @@ -913,16 +920,19 @@ impl RpcServerImpl { let mut module = self.clone().into_rpc(); let methods: Vec = module.method_names().map(|s| s.to_string()).collect(); - module.register_method("rpc.discover", move |_, _| { - serde_json::json!({ "methods": methods }) - }).expect("register rpc.discover"); + module + .register_method( + "rpc.discover", + move |_, _| serde_json::json!({ "methods": methods }), + ) + .expect("register rpc.discover"); #[cfg(feature = "schema")] { let spec = crate::rpc_schema::full_spec(); - module.register_method("rpc.schema", move |_, _| { - spec.clone() - }).expect("register rpc.schema"); + module + .register_method("rpc.schema", move |_, _| spec.clone()) + .expect("register rpc.schema"); } let handle = listener.start(module); @@ -1028,7 +1038,11 @@ impl RpcServer for RpcServerImpl { Ok(spaceout) } - async fn get_commitment(&self, subject: Subject, root: Option) -> Result, ErrorObjectOwned> { + async fn get_commitment( + &self, + subject: Subject, + root: Option, + ) -> Result, ErrorObjectOwned> { let c = self .store .get_commitment(subject, root.map(|r| *r.as_ref())) @@ -1055,7 +1069,6 @@ impl RpcServer for RpcServerImpl { Ok(delegator) } - async fn check_package( &self, txs: Vec, @@ -1152,7 +1165,7 @@ impl RpcServer for RpcServerImpl { subject: Subject, message: Bytes, ) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_sign_schnorr(subject, message.to_vec()) .await @@ -1165,7 +1178,7 @@ impl RpcServer for RpcServerImpl { wallet: &str, subject: Subject, ) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_can_operate(subject) .await @@ -1189,7 +1202,7 @@ impl RpcServer for RpcServerImpl { &self, wallet: &str, ) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_get_info() .await @@ -1228,7 +1241,7 @@ impl RpcServer for RpcServerImpl { request: RpcWalletTxBuilder, ) -> Result { let result = self - .wallet(&wallet) + .wallet(wallet) .await? .send_batch_tx(request) .await @@ -1241,7 +1254,7 @@ impl RpcServer for RpcServerImpl { wallet: &str, kind: AddressKind, ) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_get_new_address(kind) .await @@ -1253,7 +1266,7 @@ impl RpcServer for RpcServerImpl { wallet: &str, kind: AddressKind, ) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_increment_address(kind) .await @@ -1267,7 +1280,7 @@ impl RpcServer for RpcServerImpl { fee_rate: FeeRate, skip_tx_check: bool, ) -> Result, ErrorObjectOwned> { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_fee_bump(txid, fee_rate, skip_tx_check) .await @@ -1281,7 +1294,7 @@ impl RpcServer for RpcServerImpl { fee_rate: Option, skip_tx_check: bool, ) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_buy(listing, fee_rate, skip_tx_check) .await @@ -1294,7 +1307,7 @@ impl RpcServer for RpcServerImpl { space: String, amount: u64, ) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_sell(space, amount) .await @@ -1332,7 +1345,7 @@ impl RpcServer for RpcServerImpl { count: usize, skip: usize, ) -> Result, ErrorObjectOwned> { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_list_transactions(count, skip) .await @@ -1345,7 +1358,7 @@ impl RpcServer for RpcServerImpl { outpoint: OutPoint, fee_rate: FeeRate, ) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_force_spend(outpoint, fee_rate) .await @@ -1356,7 +1369,7 @@ impl RpcServer for RpcServerImpl { &self, wallet: &str, ) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_list_spaces() .await @@ -1369,7 +1382,7 @@ impl RpcServer for RpcServerImpl { kind: Option, ) -> Result { let external = kind.as_deref() == Some("external"); - self.wallet(&wallet) + self.wallet(wallet) .await? .send_list_nums(external) .await @@ -1380,7 +1393,7 @@ impl RpcServer for RpcServerImpl { &self, wallet: &str, ) -> Result, ErrorObjectOwned> { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_list_unspent() .await @@ -1388,7 +1401,7 @@ impl RpcServer for RpcServerImpl { } async fn wallet_list_bidouts(&self, wallet: &str) -> Result, ErrorObjectOwned> { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_list_bidouts() .await @@ -1396,34 +1409,42 @@ impl RpcServer for RpcServerImpl { } async fn wallet_get_balance(&self, wallet: &str) -> Result { - self.wallet(&wallet) + self.wallet(wallet) .await? .send_get_balance() .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } - async fn get_fallback(&self, subject: Subject) -> Result, ErrorObjectOwned> { - let data = match &subject { - Subject::Label(label) if !label.is_numeric() => { - let space_hash = SpaceKey::from(Sha256::hash(label.as_ref())); - let fso = self.store.get_space(space_hash).await - .map_err(|e| ErrorObjectOwned::owned(-1, e.to_string(), None::))?; - fso.and_then(|fso| { - if let Some(space) = &fso.spaceout.space { - if let Covenant::Transfer { data, .. } = &space.covenant { - return data.as_ref().map(|b| b.clone().to_vec()); + async fn get_fallback( + &self, + subject: Subject, + ) -> Result, ErrorObjectOwned> { + let data = + match &subject { + Subject::Label(label) if !label.is_numeric() => { + let space_hash = SpaceKey::from(Sha256::hash(label.as_ref())); + let fso = + self.store.get_space(space_hash).await.map_err(|e| { + ErrorObjectOwned::owned(-1, e.to_string(), None::) + })?; + fso.and_then(|fso| { + if let Some(space) = &fso.spaceout.space { + if let Covenant::Transfer { data, .. } = &space.covenant { + return data.as_ref().map(|b| b.clone().to_vec()); + } } - } - None - }) - } - _ => { - let fpt = self.store.get_ptr(subject).await - .map_err(|e| ErrorObjectOwned::owned(-1, e.to_string(), None::))?; - fpt.and_then(|fpt| fpt.numout.num.data.map(|b| b.to_vec())) - } - }; + None + }) + } + _ => { + let fpt = + self.store.get_ptr(subject).await.map_err(|e| { + ErrorObjectOwned::owned(-1, e.to_string(), None::) + })?; + fpt.and_then(|fpt| fpt.numout.num.data.map(|b| b.to_vec())) + } + }; match data { None => Ok(None), @@ -1440,16 +1461,28 @@ impl RpcServer for RpcServerImpl { } } - async fn debug_set_expire_height(&self, space: &str, expire_height: u32) -> Result<(), ErrorObjectOwned> { + async fn debug_set_expire_height( + &self, + space: &str, + expire_height: u32, + ) -> Result<(), ErrorObjectOwned> { // Only allow on regtest - let info = self.store.get_server_info().await + let info = self + .store + .get_server_info() + .await .map_err(|e| ErrorObjectOwned::owned(-1, e.to_string(), None::))?; if info.network != ExtendedNetwork::Regtest { - return Err(ErrorObjectOwned::owned(-1, "debug_set_expire_height is only available on regtest", None::)); + return Err(ErrorObjectOwned::owned( + -1, + "debug_set_expire_height is only available on regtest", + None::, + )); } - let space_label = SLabel::from_str(space) - .map_err(|e| ErrorObjectOwned::owned(-1, format!("Invalid space name: {}", e), None::))?; + let space_label = SLabel::from_str(space).map_err(|e| { + ErrorObjectOwned::owned(-1, format!("Invalid space name: {}", e), None::) + })?; self.store .debug_set_expire_height(space_label, expire_height) @@ -1470,7 +1503,7 @@ impl AsyncChainState { rpc: &BitcoinRpc, ) -> Result, anyhow::Error> { let info: serde_json::Value = rpc - .send_json(client, &rpc.get_raw_transaction(&txid, true)) + .send_json(client, &rpc.get_raw_transaction(txid, true)) .await .map_err(|e| anyhow!("Could not retrieve tx ({})", e))?; @@ -1478,13 +1511,8 @@ impl AsyncChainState { BlockHash::from_str(info.get("blockhash").and_then(|t| t.as_str()).ok_or_else( || anyhow!("Could not retrieve block hash for tx (is it in the mempool?)"), )?)?; - let block = Self::get_indexed_block( - state, - HeightOrHash::Hash(block_hash), - client, - rpc, - ) - .await?; + let block = + Self::get_indexed_block(state, HeightOrHash::Hash(block_hash), client, rpc).await?; Ok(block .block_meta @@ -1633,16 +1661,22 @@ impl AsyncChainState { let _ = resp.send(result); } ChainStateCommand::GetNum { subject, resp } => { - let result = resolve_num_id(state, &subject) - .and_then(|id| state.get_num_info(&id)); + let result = resolve_num_id(state, &subject).and_then(|id| state.get_num_info(&id)); let _ = resp.send(result); } ChainStateCommand::GetNumOutpoint { subject, resp } => { - let result = resolve_num_id(state, &subject) - .and_then(|id| state.get_num_outpoint_by_id(&id).context("could not fetch numout")); + let result = resolve_num_id(state, &subject).and_then(|id| { + state + .get_num_outpoint_by_id(&id) + .context("could not fetch numout") + }); let _ = resp.send(result); } - ChainStateCommand::GetCommitment { subject, root, resp } => { + ChainStateCommand::GetCommitment { + subject, + root, + resp, + } => { let result = get_commitment(state, &subject, root); let _ = resp.send(result); } @@ -1651,9 +1685,11 @@ impl AsyncChainState { let _ = resp.send(result); } ChainStateCommand::GetDelegator { subject, resp } => { - let result = resolve_num_id(state, &subject) - .and_then(|id| state.get_delegator(&DelegatorKey::from_id::(id)) - .map_err(|e| anyhow!("could not get delegator: {}", e))); + let result = resolve_num_id(state, &subject).and_then(|id| { + state + .get_delegator(&DelegatorKey::from_id::(id)) + .map_err(|e| anyhow!("could not get delegator: {}", e)) + }); let _ = resp.send(result); } ChainStateCommand::GetNumOut { outpoint, resp } => { @@ -1666,18 +1702,14 @@ impl AsyncChainState { height_or_hash, resp, } => { - let res = - Self::get_indexed_block(state, height_or_hash, client, rpc) - .await; + let res = Self::get_indexed_block(state, height_or_hash, client, rpc).await; let _ = resp.send(res); } ChainStateCommand::GetNumBlockMeta { height_or_hash, resp, } => { - let res = - Self::get_indexed_ptr_block(state, height_or_hash, client, rpc) - .await; + let res = Self::get_indexed_ptr_block(state, height_or_hash, client, rpc).await; let _ = resp.send(res); } ChainStateCommand::GetTxMeta { txid, resp } => { @@ -1693,11 +1725,14 @@ impl AsyncChainState { _ = resp.send(rollouts); } ChainStateCommand::VerifyListing { listing, resp } => { - _ = resp.send( - SpacesWallet::verify_listing::(state, &listing).map(|_| ()), - ); + _ = resp.send(SpacesWallet::verify_listing::(state, &listing).map(|_| ())); } - ChainStateCommand::VerifySchnorr { subject, message, signature, resp } => { + ChainStateCommand::VerifySchnorr { + subject, + message, + signature, + resp, + } => { let result = (|| { let sig = schnorr::Signature::from_slice(&signature) .map_err(|_| anyhow!("Invalid signature format"))?; @@ -1719,8 +1754,16 @@ impl AsyncChainState { ChainStateCommand::GetRootAnchors { resp } => { _ = resp.send(Self::handle_get_anchor(anchors_path, state)); } - ChainStateCommand::DebugSetExpireHeight { space, expire_height, resp } => { - _ = resp.send(Self::handle_debug_set_expire_height(state, space, expire_height)); + ChainStateCommand::DebugSetExpireHeight { + space, + expire_height, + resp, + } => { + _ = resp.send(Self::handle_debug_set_expire_height( + state, + space, + expire_height, + )); } } } @@ -1731,18 +1774,26 @@ impl AsyncChainState { expire_height: u32, ) -> anyhow::Result<()> { let space_key = SpaceKey::from(Sha256::hash(space.as_ref())); - let outpoint = state.get_space_outpoint(&space_key)? + let outpoint = state + .get_space_outpoint(&space_key)? .ok_or_else(|| anyhow::anyhow!("Space not found: {}", space))?; - let mut spaceout = state.get_spaceout(&outpoint)? + let mut spaceout = state + .get_spaceout(&outpoint)? .ok_or_else(|| anyhow::anyhow!("Spaceout not found for outpoint"))?; // Update expire_height in the covenant if let Some(ref mut space_data) = spaceout.space { match &mut space_data.covenant { - Covenant::Transfer { expire_height: ref mut eh, .. } => { + Covenant::Transfer { + expire_height: eh, .. + } => { *eh = expire_height; } - _ => return Err(anyhow::anyhow!("Space is not in Transfer covenant (not owned)")), + _ => { + return Err(anyhow::anyhow!( + "Space is not in Transfer covenant (not owned)" + )); + } } } else { return Err(anyhow::anyhow!("SpaceOut has no space data")); @@ -1761,9 +1812,9 @@ impl AsyncChainState { if let Some(anchors_path) = anchors_path { let anchors: Vec = serde_json::from_reader( File::open(anchors_path) - .or_else(|e| Err(anyhow!("Could not open anchors file: {}", e)))?, + .map_err(|e| anyhow!("Could not open anchors file: {}", e))?, ) - .or_else(|e| Err(anyhow!("Could not read anchors file: {}", e)))?; + .map_err(|e| anyhow!("Could not read anchors file: {}", e))?; return Ok(anchors); } @@ -1773,7 +1824,10 @@ impl AsyncChainState { // Try to compute PTR root if we're past PTR genesis let ptrs_root = if state.can_scan_nums(meta.height) { - state.nums_mut().state.inner() + state + .nums_mut() + .state + .inner() .ok() .and_then(|s| s.compute_root().ok()) } else { @@ -1823,8 +1877,8 @@ impl AsyncChainState { space_tree_keys.insert(outpoint_key.into()); if let Some(space) = &fso.spaceout.space { if let Covenant::Transfer { expire_height, .. } = &space.covenant { - let last_update = expire_height - .saturating_sub(spaces_protocol::constants::RENEWAL_INTERVAL); + let last_update = + expire_height.saturating_sub(spaces_protocol::constants::RENEWAL_INTERVAL); most_recent_update = std::cmp::max(most_recent_update, last_update); } } @@ -1838,44 +1892,45 @@ impl AsyncChainState { NumKeyKind::Num(numeric) => { let id = state.get_num_id(&numeric)?; if let Some(id) = id { - let fpt = state.get_num_info(&id)? + let fpt = state + .get_num_info(&id)? .expect("num id must exist if numeric exists"); - num_tree_keys.insert( - NumOutpointKey::from_outpoint::(fpt.outpoint()).into() - ); - most_recent_update = std::cmp::max(most_recent_update, fpt.numout.num.last_update); + num_tree_keys + .insert(NumOutpointKey::from_outpoint::(fpt.outpoint()).into()); + most_recent_update = + std::cmp::max(most_recent_update, fpt.numout.num.last_update); // insert delegate information let operator_id = NumId::from_spk::(fpt.numout.script_pubkey); let operator = state.get_num_info(&operator_id)?; if let Some(operator) = operator { num_tree_keys.insert( - NumOutpointKey::from_outpoint::(operator.outpoint()).into() + NumOutpointKey::from_outpoint::(operator.outpoint()).into(), ); - most_recent_update = std::cmp::max(most_recent_update, operator.numout.num.last_update); - + most_recent_update = + std::cmp::max(most_recent_update, operator.numout.num.last_update); } else { num_tree_keys.insert(operator_id.into()); } - } } NumKeyKind::Id(id) => { if let Some(fpt) = state.get_num_info(&id)? { - num_tree_keys.insert( - NumOutpointKey::from_outpoint::(fpt.outpoint()).into() - ); - most_recent_update = std::cmp::max(most_recent_update, fpt.numout.num.last_update); + num_tree_keys + .insert(NumOutpointKey::from_outpoint::(fpt.outpoint()).into()); + most_recent_update = + std::cmp::max(most_recent_update, fpt.numout.num.last_update); // insert delegate information let operator_id = NumId::from_spk::(fpt.numout.script_pubkey); let operator = state.get_num_info(&operator_id)?; if let Some(operator) = operator { num_tree_keys.insert( - NumOutpointKey::from_outpoint::(operator.outpoint()).into() + NumOutpointKey::from_outpoint::(operator.outpoint()).into(), ); - most_recent_update = std::cmp::max(most_recent_update, operator.numout.num.last_update); + most_recent_update = + std::cmp::max(most_recent_update, operator.numout.num.last_update); } else { num_tree_keys.insert(operator_id.into()); } @@ -1886,10 +1941,10 @@ impl AsyncChainState { } NumKeyKind::Commitment(k) => { num_tree_keys.insert(k.into()); - }, + } NumKeyKind::CommitmentTip(k) => { num_tree_keys.insert(k.into()); - }, + } } } @@ -1900,23 +1955,25 @@ impl AsyncChainState { let blocks_remaining = next_commit - tip.height; return Err(anyhow!( "Cannot prove: data updated at block {} is not yet committed. Try again in {} block(s)", - most_recent_update, blocks_remaining + most_recent_update, + blocks_remaining )); } - let num_tree_keys : Vec<_> = num_tree_keys.into_iter().collect(); - let space_tree_keys : Vec<_> = space_tree_keys.into_iter().collect(); + let num_tree_keys: Vec<_> = num_tree_keys.into_iter().collect(); + let space_tree_keys: Vec<_> = space_tree_keys.into_iter().collect(); let cached_height = Self::cached_snapshot_height(tip.height); - let use_cached = !prefer_recent - && cached_height.is_some_and(|h| most_recent_update <= h); + let use_cached = !prefer_recent && cached_height.is_some_and(|h| most_recent_update <= h); let (spaces_proof, spaces_root, block_anchor, ptrs_proof, ptrs_root) = if use_cached { let height = cached_height.unwrap(); let snapshot = state.snapshot_at(height)?; let spaces_anchor: ChainAnchor = snapshot.spaces.metadata().try_into()?; - let spaces_proof = snapshot.spaces.prove(&space_tree_keys, ProofType::Standard)?; + let spaces_proof = snapshot + .spaces + .prove(&space_tree_keys, ProofType::Standard)?; let spaces_root = spaces_proof.compute_root()?; let ptrs_anchor: ChainAnchor = snapshot.nums.metadata().try_into()?; @@ -1929,7 +1986,13 @@ impl AsyncChainState { let ptrs_proof = snapshot.nums.prove(&num_tree_keys, ProofType::Standard)?; let ptrs_root = ptrs_proof.compute_root()?; - (spaces_proof, spaces_root, spaces_anchor, ptrs_proof, ptrs_root) + ( + spaces_proof, + spaces_root, + spaces_anchor, + ptrs_proof, + ptrs_root, + ) } else { let spaces_snapshot = state.spaces_inner()?; let spaces_root = spaces_snapshot.compute_root()?; @@ -1948,7 +2011,13 @@ impl AsyncChainState { let ptrs_proof = ptrs_snapshot.prove(&num_tree_keys, ProofType::Standard)?; let ptrs_root = ptrs_proof.compute_root()?; - (spaces_proof, spaces_root, spaces_anchor, ptrs_proof, ptrs_root) + ( + spaces_proof, + spaces_root, + spaces_anchor, + ptrs_proof, + ptrs_root, + ) }; let spaces_buf = spaces_proof.to_vec()?; @@ -2044,10 +2113,18 @@ impl AsyncChainState { resp_rx.await? } - pub async fn debug_set_expire_height(&self, space: SLabel, expire_height: u32) -> anyhow::Result<()> { + pub async fn debug_set_expire_height( + &self, + space: SLabel, + expire_height: u32, + ) -> anyhow::Result<()> { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(ChainStateCommand::DebugSetExpireHeight { space, expire_height, resp }) + .send(ChainStateCommand::DebugSetExpireHeight { + space, + expire_height, + resp, + }) .await?; resp_rx.await? } @@ -2127,10 +2204,18 @@ impl AsyncChainState { resp_rx.await? } - pub async fn get_commitment(&self, subject: Subject, root: Option) -> anyhow::Result> { + pub async fn get_commitment( + &self, + subject: Subject, + root: Option, + ) -> anyhow::Result> { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(ChainStateCommand::GetCommitment { subject, root, resp }) + .send(ChainStateCommand::GetCommitment { + subject, + root, + resp, + }) .await?; resp_rx.await? } @@ -2193,7 +2278,8 @@ fn resolve_num_id(state: &mut Chain, subject: &Subject) -> anyhow::Result Subject::NumId(id) => Ok(*id), Subject::Label(label) if label.is_numeric() => { let numeric: SNumeric = label.clone().try_into().unwrap(); - state.get_num_id(&numeric)? + state + .get_num_id(&numeric)? .ok_or_else(|| anyhow!("numeric '{}' not found", numeric)) } Subject::Label(_) => Err(anyhow!("expected a num id or numeric, not a space")), @@ -2259,12 +2345,12 @@ async fn get_server_info( }) } - fn resolve_label(state: &mut Chain, subject: &Subject) -> anyhow::Result { match subject { Subject::Label(label) => Ok(label.clone()), Subject::NumId(id) => { - let info = state.get_num_info(id)? + let info = state + .get_num_info(id)? .ok_or_else(|| anyhow!("num id '{}' not found", id))?; Ok(info.numout.num.name.to_slabel()) } @@ -2281,18 +2367,22 @@ fn get_delegation(state: &mut Chain, subject: &Subject) -> anyhow::Result