From cd5b973e4d86d1157dcf04bef6e035d67f8169b1 Mon Sep 17 00:00:00 2001 From: raph Date: Mon, 3 Mar 2025 19:55:30 +0100 Subject: [PATCH 1/6] Migrate create, restore, and receive to bdk (#4231) --- .github/workflows/ci.yaml | 15 +- Cargo.lock | 68 ++++ Cargo.toml | 1 + bin/test | 16 + blacklist.txt | 246 ++++++++++++ justfile | 3 + src/lib.rs | 2 + src/subcommand/wallet.rs | 34 +- src/subcommand/wallet/create.rs | 9 +- src/subcommand/wallet/descriptors.rs | 14 + src/subcommand/wallet/dump.rs | 16 - src/subcommand/wallet/receive.rs | 25 +- src/subcommand/wallet/restore.rs | 77 +--- src/wallet.rs | 535 ++++++++++++--------------- src/wallet/persister.rs | 54 +++ src/wallet/wallet_constructor.rs | 238 ++---------- tests/lib.rs | 7 +- tests/test_server.rs | 18 +- tests/wallet.rs | 1 - tests/wallet/create.rs | 144 +++++-- tests/wallet/dump.rs | 77 ---- tests/wallet/receive.rs | 21 +- tests/wallet/restore.rs | 341 +++-------------- 23 files changed, 931 insertions(+), 1031 deletions(-) create mode 100755 bin/test create mode 100644 blacklist.txt create mode 100644 src/subcommand/wallet/descriptors.rs delete mode 100644 src/subcommand/wallet/dump.rs create mode 100644 src/wallet/persister.rs delete mode 100644 tests/wallet/dump.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0a06d556a6..d7ed72d1a5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,8 +5,6 @@ on: branches: - master pull_request: - branches: - - master defaults: run: @@ -94,6 +92,14 @@ jobs: sudo apt-get install ripgrep ./bin/forbid + - name: Check for blacklist + if: ${{ github.event.pull_request.base.ref == 'master' }} + run: | + if [[ -f blacklist.txt ]]; then + echo "error: found blacklist.txt" + exit 1 + fi + test: strategy: matrix: @@ -116,4 +122,9 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Test + if: ${{ github.event.pull_request.base.ref == 'master' }} run: cargo test --all + + - name: Test + if: ${{ github.event.pull_request.base.ref == 'bdk' }} + run: ./bin/test diff --git a/Cargo.lock b/Cargo.lock index 12e208f3f3..43a98d084f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -471,12 +483,55 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bdk_chain" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4955734f97b2baed3f36d16ae7c203fdde31ae85391ac44ee3cbcaf0886db5ce" +dependencies = [ + "bdk_core", + "bitcoin", + "miniscript", + "serde", +] + +[[package]] +name = "bdk_core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b545aea1efc090e4f71f1dd5468090d9f54c3de48002064c04895ef811fbe0b2" +dependencies = [ + "bitcoin", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "bdk_wallet" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a13c947be940d32a91b876fc5223a6d839a40bc219496c5c78af74714b1b3f7" +dependencies = [ + "bdk_chain", + "bitcoin", + "miniscript", + "rand_core", + "serde", + "serde_json", +] + [[package]] name = "bech32" version = "0.11.0" @@ -535,6 +590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", + "base64 0.21.7", "bech32", "bitcoin-internals 0.3.0", "bitcoin-io", @@ -1536,6 +1592,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "serde", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -2260,6 +2326,7 @@ checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" dependencies = [ "bech32", "bitcoin", + "serde", ] [[package]] @@ -2546,6 +2613,7 @@ dependencies = [ "axum", "axum-server", "base64 0.22.1", + "bdk_wallet", "bip322", "bip39", "bitcoin", diff --git a/Cargo.toml b/Cargo.toml index 327786ac6b..2012257ae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ anyhow = { version = "1.0.90", features = ["backtrace"] } axum = { version = "0.8.1", features = ["http2"] } axum-server = "0.7.1" base64.workspace = true +bdk_wallet = "1.1.0" bip322 = "0.0.9" bip39 = "2.0.0" bitcoin.workspace = true diff --git a/bin/test b/bin/test new file mode 100755 index 0000000000..62b1dc7f78 --- /dev/null +++ b/bin/test @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import shutil, subprocess, sys + +test = 'ltest' if shutil.which('cargo-ltest') else 'test' + +with open('blacklist.txt') as f: + blacklist = f.read().splitlines() + +command = ['cargo', test, '--', '--exact'] + +for test in blacklist: + command.append('--skip') + command.append(test) + +sys.exit(subprocess.run(command).returncode) diff --git a/blacklist.txt b/blacklist.txt new file mode 100644 index 0000000000..1a459d64b7 --- /dev/null +++ b/blacklist.txt @@ -0,0 +1,246 @@ +balances::with_runes +decode::from_core +index::export_inscription_number_to_id_tsv +json_api::get_decode_tx +json_api::get_inscription +json_api::get_inscription_with_metaprotocol +json_api::get_inscriptions +json_api::get_inscriptions_in_block +json_api::get_output +json_api::get_runes +json_api::get_sat_with_inscription_and_sat_index +json_api::get_sat_with_inscription_on_common_sat_and_more_inscriptions +json_api::get_status +json_api::outputs_address +runes::one_rune +runes::two_runes +server::address_page_shows_aggregated_inscriptions +server::address_page_shows_aggregated_runes_balance +server::address_page_shows_multiple_runes +server::address_page_shows_outputs_and_sat_balance +server::address_page_shows_single_rune +server::inscription_appears_on_output_page +server::inscription_appears_on_reveal_transaction_page +server::inscription_content +server::inscription_metadata +server::inscription_page +server::inscription_page_after_send +server::inscription_transactions_are_stored_with_transaction_index +server::inscriptions_page +server::inscriptions_page_has_next_and_previous +server::inscriptions_page_is_sorted +server::multiple_inscriptions_appear_on_reveal_transaction_page +server::recursive_inscription_endpoint +wallet::addresses::addresses +wallet::authentication::authentication +wallet::balance::inscribed_utxos_are_deducted_from_cardinal +wallet::balance::runic_utxos_are_deducted_from_cardinal +wallet::balance::runic_utxos_are_displayed_with_decimal_amount +wallet::balance::unsynced_wallet_fails_with_unindexed_output +wallet::balance::wallet_balance +wallet::batch_command::batch_can_etch_rune +wallet::batch_command::batch_can_etch_rune_without_premine +wallet::batch_command::batch_can_etch_turbo_rune +wallet::batch_command::batch_in_same_output_but_different_satpoints +wallet::batch_command::batch_in_same_output_with_non_default_postage +wallet::batch_command::batch_in_separate_outputs_with_parent +wallet::batch_command::batch_in_separate_outputs_with_parent_and_non_default_postage +wallet::batch_command::batch_inscribe_and_etch_with_two_parents +wallet::batch_command::batch_inscribe_can_create_inscription_with_gallery +wallet::batch_command::batch_inscribe_can_create_one_inscription +wallet::batch_command::batch_inscribe_can_etch_rune_with_height +wallet::batch_command::batch_inscribe_can_etch_rune_with_offset +wallet::batch_command::batch_inscribe_errors_if_pending_etchings +wallet::batch_command::batch_inscribe_fails_if_batchfile_has_no_inscriptions +wallet::batch_command::batch_inscribe_fails_if_gallery_inscription_does_not_exist +wallet::batch_command::batch_inscribe_fails_if_invalid_network_destination_address +wallet::batch_command::batch_inscribe_fails_with_shared_output_or_same_sat_and_destination_set +wallet::batch_command::batch_inscribe_inscriptions_with_multiple_parents +wallet::batch_command::batch_inscribe_respects_dry_run_flag +wallet::batch_command::batch_inscribe_with_delegate_inscription +wallet::batch_command::batch_inscribe_with_fee_rate +wallet::batch_command::batch_inscribe_with_multiple_inscriptions +wallet::batch_command::batch_inscribe_with_multiple_inscriptions_with_parent +wallet::batch_command::batch_inscribe_with_non_existent_delegate_inscription +wallet::batch_command::batch_inscribe_with_sat_arg_fails_if_wrong_mode +wallet::batch_command::batch_inscribe_with_sat_argument_with_parent +wallet::batch_command::batch_inscribe_with_satpoint +wallet::batch_command::batch_inscribe_with_satpoints_with_different_sizes +wallet::batch_command::batch_inscribe_with_satpoints_with_parent +wallet::batch_command::batch_inscribe_works_with_some_destinations_set_and_others_not +wallet::batch_command::batch_same_sat +wallet::batch_command::batch_same_sat_with_parent +wallet::batch_command::batch_same_sat_with_satpoint_and_reinscription +wallet::batch_command::etch_divisibility_over_maximum_error +wallet::batch_command::etch_existing_rune_error +wallet::batch_command::etch_mintable_overflow_error +wallet::batch_command::etch_mintable_plus_premine_overflow_error +wallet::batch_command::etch_requires_rune_index +wallet::batch_command::etch_reserved_rune_error +wallet::batch_command::etch_sub_minimum_rune_error +wallet::batch_command::forbid_etching_below_rune_activation_height +wallet::batch_command::incorrect_supply_error +wallet::batch_command::invalid_end_height_error +wallet::batch_command::invalid_start_height_error +wallet::batch_command::oversize_runestone_error +wallet::batch_command::oversize_runestones_are_allowed_with_no_limit +wallet::batch_command::zero_amount_error +wallet::batch_command::zero_cap_error +wallet::batch_command::zero_height_interval_error +wallet::batch_command::zero_offset_interval_error +wallet::batch_command::zero_supply_error +wallet::burn::burn_rune +wallet::burn::burn_rune_with_many_assets_in_wallet +wallet::burn::burning_rune_creates_change_output_for_non_burnt_runes +wallet::burn::burns_only_one_sat +wallet::burn::cannot_burn_inscription_sharing_utxo_with_another_inscription +wallet::burn::cbor_and_json_metadata_flags_conflict +wallet::burn::cbor_metadata_can_be_included_when_burning +wallet::burn::inscriptions_can_be_burned +wallet::burn::json_metadata_can_be_included_when_burning +wallet::burn::oversize_metadata_requires_no_limit_flag +wallet::burn::runic_outputs_are_protected +wallet::cardinals::cardinals +wallet::cardinals::cardinals_does_not_show_runic_outputs +wallet::inscribe::cbor_metadata_appears_on_inscription_page +wallet::inscribe::error_message_when_parsing_cbor_metadata_is_reasonable +wallet::inscribe::error_message_when_parsing_json_metadata_is_reasonable +wallet::inscribe::file_inscribe_with_delegate_inscription +wallet::inscribe::file_inscribe_with_non_existent_delegate_inscription +wallet::inscribe::file_inscribe_with_only_delegate +wallet::inscribe::inscribe_can_compress +wallet::inscribe::inscribe_can_include_gallery_items +wallet::inscribe::inscribe_creates_inscriptions +wallet::inscribe::inscribe_does_not_pick_locked_utxos +wallet::inscribe::inscribe_does_not_use_inscribed_sats_as_cardinal_utxos +wallet::inscribe::inscribe_exceeds_chain_limit +wallet::inscribe::inscribe_fails_if_bitcoin_core_is_too_old +wallet::inscribe::inscribe_fails_if_gallery_inscription_does_not_exist +wallet::inscribe::inscribe_no_backup +wallet::inscribe::inscribe_to_address_on_different_network +wallet::inscribe::inscribe_to_specific_destination +wallet::inscribe::inscribe_unknown_file_extension +wallet::inscribe::inscribe_with_commit_fee_rate +wallet::inscribe::inscribe_with_dry_run_flag +wallet::inscribe::inscribe_with_dry_run_flag_fees_increase +wallet::inscribe::inscribe_with_fee_rate +wallet::inscribe::inscribe_with_no_limit +wallet::inscribe::inscribe_with_non_existent_parent_inscription +wallet::inscribe::inscribe_with_optional_satpoint_arg +wallet::inscribe::inscribe_with_parent_inscription_and_fee_rate +wallet::inscribe::inscribe_with_sat_arg +wallet::inscribe::inscribe_with_sat_arg_fails_if_no_index_or_not_found +wallet::inscribe::inscribe_with_wallet_named_foo +wallet::inscribe::inscribe_works_with_huge_expensive_inscriptions +wallet::inscribe::inscribe_works_with_postage +wallet::inscribe::inscription_with_delegate_returns_effective_content_type +wallet::inscribe::inscriptions_are_not_compressed_if_no_space_is_saved_by_compression +wallet::inscribe::json_metadata_appears_on_inscription_page +wallet::inscribe::mainnet_has_no_content_size_limit +wallet::inscribe::metaprotocol_appears_on_inscription_page +wallet::inscribe::no_metadata_appears_on_inscription_page_if_no_metadata_is_passed +wallet::inscribe::refuse_to_inscribe_already_inscribed_utxo +wallet::inscribe::refuse_to_reinscribe_sats +wallet::inscribe::regtest_has_no_content_size_limit +wallet::inscribe::reinscribe_with_flag +wallet::inscribe::server_can_decompress_brotli +wallet::inscribe::try_reinscribe_without_flag +wallet::inscribe::with_reinscribe_flag_but_not_actually_a_reinscription +wallet::inscriptions::inscriptions +wallet::inscriptions::inscriptions_includes_locked_utxos +wallet::inscriptions::inscriptions_with_postage +wallet::label::label +wallet::mint::minting_is_allowed_when_mint_begins_next_block +wallet::mint::minting_rune_and_fails_if_after_end +wallet::mint::minting_rune_and_then_sending_works +wallet::mint::minting_rune_fails_if_not_mintable +wallet::mint::minting_rune_with_destination +wallet::mint::minting_rune_with_no_rune_index_fails +wallet::mint::minting_rune_with_postage +wallet::mint::minting_rune_with_postage_dust +wallet::offer::accept::accepted_offer_works +wallet::offer::accept::buyer_inputs_must_be_signed +wallet::offer::accept::error_on_base64_psbt_decode +wallet::offer::accept::error_on_psbt_deserialize +wallet::offer::accept::expected_outgoing_inscription +wallet::offer::accept::must_have_inscription_index_to_accept +wallet::offer::accept::outgoing_does_not_contain_runes +wallet::offer::accept::outgoing_may_not_contain_more_than_one_inscription +wallet::offer::accept::outgoing_may_not_contain_no_inscriptions +wallet::offer::accept::psbt_may_not_contain_more_than_one_input_owned_by_wallet +wallet::offer::accept::psbt_may_not_contain_no_inputs_owned_by_wallet +wallet::offer::accept::seller_input_must_not_be_signed +wallet::offer::accept::unexpected_balance_change +wallet::offer::create::created_offer_is_correct +wallet::offer::create::inscription_must_exist +wallet::offer::create::inscription_must_have_valid_address +wallet::offer::create::inscription_must_not_be_in_wallet +wallet::outputs::outputs +wallet::outputs::outputs_includes_locked_outputs +wallet::outputs::outputs_includes_runes_and_inscriptions +wallet::outputs::outputs_includes_sat_ranges +wallet::outputs::outputs_includes_unbound_outputs +wallet::pending::wallet_pending +wallet::resume::commitment_output_is_locked +wallet::resume::resume_suspended +wallet::resume::wallet_resume +wallet::resume::wallet_resume_by_rune_name +wallet::resume::wallet_resume_by_rune_not_found +wallet::runics::wallet_runics +wallet::sats::requires_sat_index +wallet::sats::sats +wallet::sats::sats_all +wallet::sats::sats_from_tsv_file_not_found +wallet::sats::sats_from_tsv_parse_error +wallet::sats::sats_from_tsv_success +wallet::selection::inscribe_does_not_select_runic_utxos +wallet::selection::mint_does_not_select_inscription +wallet::selection::offer_create_does_not_select_non_cardinal_utxos +wallet::selection::send_amount_does_not_select_runic_utxos +wallet::selection::send_inscription_does_not_select_runic_utxos +wallet::selection::send_satpoint_does_not_send_runic_utxos +wallet::selection::sending_rune_does_not_send_inscription +wallet::selection::split_does_not_select_inscribed_or_runic_utxos +wallet::send::can_send_after_dust_limit_from_an_inscription +wallet::send::do_not_send_within_dust_limit_of_an_inscription +wallet::send::error_messages_use_spaced_runes +wallet::send::inscriptions_can_be_sent +wallet::send::inscriptions_cannot_be_sent_by_satpoint +wallet::send::send_addresses_must_be_valid_for_network +wallet::send::send_btc_does_not_send_locked_utxos +wallet::send::send_btc_fails_if_lock_unspent_fails +wallet::send::send_btc_locks_inscriptions +wallet::send::send_btc_with_fee_rate +wallet::send::send_does_not_use_inscribed_sats_as_cardinal_utxos +wallet::send::send_dry_run +wallet::send::send_inscribed_inscription +wallet::send::send_inscription_by_sat +wallet::send::send_on_mainnnet_works_with_wallet_named_foo +wallet::send::send_on_mainnnet_works_with_wallet_named_ord +wallet::send::send_uninscribed_sat +wallet::send::send_unknown_inscription +wallet::send::sending_rune_creates_change_output_for_non_outgoing_runes +wallet::send::sending_rune_creates_transaction_with_expected_runestone +wallet::send::sending_rune_leaves_unspent_runes_in_wallet +wallet::send::sending_rune_that_has_not_been_etched_is_an_error +wallet::send::sending_rune_with_change_works +wallet::send::sending_rune_with_divisibility_works +wallet::send::sending_rune_with_excessive_precision_is_an_error +wallet::send::sending_rune_with_insufficient_balance_is_an_error +wallet::send::sending_rune_works +wallet::send::sending_spaced_rune_works_with_no_change +wallet::send::splitting_merged_inscriptions_is_possible +wallet::send::user_must_provide_fee_rate_to_send +wallet::send::wallet_send_with_fee_rate +wallet::send::wallet_send_with_fee_rate_and_target_postage +wallet::sign::sign +wallet::sign::sign_file +wallet::sign::sign_for_inscription +wallet::sign::sign_for_output +wallet::split::cannot_split_un_etched_runes +wallet::split::oversize_op_returns_are_allowed_with_flag +wallet::split::requires_rune_index +wallet::split::simple_split +wallet::split::unrecognized_fields_are_forbidden +wallet::transactions::transactions +wallet::transactions::transactions_with_limit diff --git a/justfile b/justfile index 380f0482be..d8e0db4e84 100644 --- a/justfile +++ b/justfile @@ -232,3 +232,6 @@ swap host: changed-files tag: git diff --name-only {{tag}} + +test: + ./bin/test diff --git a/src/lib.rs b/src/lib.rs index d951a5ac98..4b895bfbb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ use { tally::Tally, }, anyhow::{anyhow, bail, ensure, Context, Error}, + bdk_wallet as bdk, bip39::Mnemonic, bitcoin::{ address::{Address, NetworkUnchecked}, @@ -58,6 +59,7 @@ use { error::{ResultExt, SnafuError}, html_escaper::{Escape, Trusted}, lazy_static::lazy_static, + miniscript::{Descriptor, DescriptorPublicKey}, ordinals::{ varint, Artifact, Charm, Edict, Epoch, Etching, Height, Pile, Rarity, Rune, RuneId, Runestone, Sat, SatPoint, SpacedRune, Terms, diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index a9ea05ca2f..a390c04f2a 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -1,6 +1,7 @@ use { super::*, - crate::wallet::{batch, wallet_constructor::WalletConstructor, ListDescriptorsResult, Wallet}, + crate::wallet::{batch, wallet_constructor::WalletConstructor, Wallet}, + bdk::KeychainKind, bitcoin::Psbt, shared_args::SharedArgs, }; @@ -11,7 +12,7 @@ mod batch_command; pub mod burn; pub mod cardinals; pub mod create; -pub mod dump; +pub mod descriptors; pub mod inscribe; pub mod inscriptions; mod label; @@ -34,8 +35,6 @@ pub mod transactions; pub(crate) struct WalletCommand { #[arg(long, default_value = "ord", help = "Use wallet named .")] pub(crate) name: String, - #[arg(long, alias = "nosync", help = "Do not update index.")] - pub(crate) no_sync: bool, #[arg( long, help = "Use ord running at . [default: http://localhost:80]" @@ -48,23 +47,23 @@ pub(crate) struct WalletCommand { #[derive(Debug, Parser)] #[allow(clippy::large_enum_variant)] pub(crate) enum Subcommand { - #[command(about = "Get wallet addresses")] + #[command(about = "List addresses")] Addresses, - #[command(about = "Get wallet balance")] + #[command(about = "Get balance")] Balance, #[command(about = "Create inscriptions and runes")] Batch(batch_command::Batch), #[command(about = "Burn an inscription")] Burn(burn::Burn), - #[command(about = "List unspent cardinal outputs in wallet")] + #[command(about = "List unspent cardinal outputs")] Cardinals, #[command(about = "Create new wallet")] Create(create::Create), - #[command(about = "Dump wallet descriptors")] - Dump, + #[command(about = "List descriptors")] + Descriptors, #[command(about = "Create inscription")] Inscribe(inscribe::Inscribe), - #[command(about = "List wallet inscriptions")] + #[command(about = "List inscriptions")] Inscriptions, #[command(about = "Export output labels")] Label, @@ -72,7 +71,7 @@ pub(crate) enum Subcommand { Mint(mint::Mint), #[command(subcommand, about = "Offer commands")] Offer(offer::Offer), - #[command(about = "List all unspent outputs in wallet")] + #[command(about = "List unspent outputs")] Outputs(outputs::Outputs), #[command(about = "List pending etchings")] Pending(pending::Pending), @@ -82,9 +81,9 @@ pub(crate) enum Subcommand { Restore(restore::Restore), #[command(about = "Resume pending etchings")] Resume(resume::Resume), - #[command(about = "List unspent runic outputs in wallet")] + #[command(about = "List unspent runic outputs")] Runics, - #[command(about = "List wallet satoshis")] + #[command(about = "List satoshis")] Sats(sats::Sats), #[command(about = "Send sat or inscription")] Send(send::Send), @@ -92,21 +91,20 @@ pub(crate) enum Subcommand { Sign(sign::Sign), #[command(about = "Split outputs")] Split(split::Split), - #[command(about = "See wallet transactions")] + #[command(about = "List transactions")] Transactions(transactions::Transactions), } impl WalletCommand { pub(crate) fn run(self, settings: Settings) -> SubcommandResult { match self.subcommand { - Subcommand::Create(create) => return create.run(self.name, &settings), - Subcommand::Restore(restore) => return restore.run(self.name, &settings), + Subcommand::Create(create) => return create.run(&settings, &self.name), + Subcommand::Restore(restore) => return restore.run(&settings, &self.name), _ => {} }; let wallet = WalletConstructor::construct( self.name.clone(), - self.no_sync, settings.clone(), self .server_url @@ -125,7 +123,7 @@ impl WalletCommand { Subcommand::Burn(burn) => burn.run(wallet), Subcommand::Cardinals => cardinals::run(wallet), Subcommand::Create(_) | Subcommand::Restore(_) => unreachable!(), - Subcommand::Dump => dump::run(wallet), + Subcommand::Descriptors => descriptors::run(wallet), Subcommand::Inscribe(inscribe) => inscribe.run(wallet), Subcommand::Inscriptions => inscriptions::run(wallet), Subcommand::Label => label::run(wallet), diff --git a/src/subcommand/wallet/create.rs b/src/subcommand/wallet/create.rs index 136b8b50e8..b49502f8d5 100644 --- a/src/subcommand/wallet/create.rs +++ b/src/subcommand/wallet/create.rs @@ -20,18 +20,13 @@ pub(crate) struct Create { } impl Create { - pub(crate) fn run(self, name: String, settings: &Settings) -> SubcommandResult { + pub(crate) fn run(self, settings: &Settings, name: &str) -> SubcommandResult { let mut entropy = [0; 16]; rand::thread_rng().fill_bytes(&mut entropy); let mnemonic = Mnemonic::from_entropy(&entropy)?; - Wallet::initialize( - name, - settings, - mnemonic.to_seed(&self.passphrase), - bitcoincore_rpc::json::Timestamp::Now, - )?; + Wallet::create(settings, name, mnemonic.to_seed(&self.passphrase))?; Ok(Some(Box::new(Output { mnemonic, diff --git a/src/subcommand/wallet/descriptors.rs b/src/subcommand/wallet/descriptors.rs new file mode 100644 index 0000000000..94faff691e --- /dev/null +++ b/src/subcommand/wallet/descriptors.rs @@ -0,0 +1,14 @@ +use super::*; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Output { + pub external: Descriptor, + pub internal: Descriptor, +} + +pub(crate) fn run(wallet: Wallet) -> SubcommandResult { + Ok(Some(Box::new(Output { + external: wallet.get_descriptor(KeychainKind::External)?, + internal: wallet.get_descriptor(KeychainKind::Internal)?, + }))) +} diff --git a/src/subcommand/wallet/dump.rs b/src/subcommand/wallet/dump.rs deleted file mode 100644 index 98a3d21516..0000000000 --- a/src/subcommand/wallet/dump.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::*; - -pub(crate) fn run(wallet: Wallet) -> SubcommandResult { - eprintln!( - "========================================== -= THIS STRING CONTAINS YOUR PRIVATE KEYS = -= DO NOT SHARE WITH ANYONE = -==========================================" - ); - - Ok(Some(Box::new( - wallet - .bitcoin_client() - .call::("listdescriptors", &[serde_json::to_value(true)?])?, - ))) -} diff --git a/src/subcommand/wallet/receive.rs b/src/subcommand/wallet/receive.rs index 0087b92529..4927a8f599 100644 --- a/src/subcommand/wallet/receive.rs +++ b/src/subcommand/wallet/receive.rs @@ -7,21 +7,24 @@ pub struct Output { #[derive(Debug, Parser)] pub(crate) struct Receive { - #[arg(short, long, help = "Generate addresses.")] - number: Option, + #[arg( + short, + long, + help = "Generate addresses.", + default_value_t = 1 + )] + number: usize, } impl Receive { - pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { - let mut addresses: Vec> = Vec::new(); + pub(crate) fn run(self, mut wallet: Wallet) -> SubcommandResult { + let addresses = wallet + .get_receive_addresses(self.number) + .into_iter() + .map(|address| address.into_unchecked()) + .collect(); - for _ in 0..self.number.unwrap_or(1) { - addresses.push( - wallet - .bitcoin_client() - .get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32m))?, - ); - } + wallet.persist()?; Ok(Some(Box::new(Output { addresses }))) } diff --git a/src/subcommand/wallet/restore.rs b/src/subcommand/wallet/restore.rs index a6eaef28e0..9298eacdf6 100644 --- a/src/subcommand/wallet/restore.rs +++ b/src/subcommand/wallet/restore.rs @@ -1,85 +1,24 @@ use super::*; -#[derive(Debug, Clone)] -pub(crate) struct Timestamp(bitcoincore_rpc::json::Timestamp); - -impl FromStr for Timestamp { - type Err = Error; - - fn from_str(s: &str) -> Result { - Ok(if s == "now" { - Self(bitcoincore_rpc::json::Timestamp::Now) - } else { - Self(bitcoincore_rpc::json::Timestamp::Time(s.parse()?)) - }) - } -} - #[derive(Debug, Parser)] pub(crate) struct Restore { - #[clap(value_enum, long, help = "Restore wallet from on stdin.")] - from: Source, #[arg(long, help = "Use when deriving wallet.")] pub(crate) passphrase: Option, - #[arg( - long, - help = "Scan chain from onwards. Can be a unix timestamp in \ - seconds or the string `now`, to skip scanning" - )] - pub(crate) timestamp: Option, -} - -#[derive(clap::ValueEnum, Debug, Clone)] -enum Source { - Descriptor, - Mnemonic, } impl Restore { - pub(crate) fn run(self, name: String, settings: &Settings) -> SubcommandResult { - ensure!( - !settings - .bitcoin_rpc_client(None)? - .list_wallet_dir()? - .iter() - .any(|wallet_name| wallet_name == &name), - "wallet `{}` already exists", - name - ); - + pub(crate) fn run(self, settings: &Settings, name: &str) -> SubcommandResult { let mut buffer = String::new(); - match self.from { - Source::Descriptor => { - io::stdin().read_to_string(&mut buffer)?; - - ensure!( - self.passphrase.is_none(), - "descriptor does not take a passphrase" - ); + io::stdin().read_line(&mut buffer)?; - ensure!( - self.timestamp.is_none(), - "descriptor does not take a timestamp" - ); + let mnemonic = Mnemonic::from_str(&buffer)?; - let wallet_descriptors: ListDescriptorsResult = serde_json::from_str(&buffer)?; - Wallet::initialize_from_descriptors(name, settings, wallet_descriptors.descriptors)?; - } - Source::Mnemonic => { - io::stdin().read_line(&mut buffer)?; - let mnemonic = Mnemonic::from_str(&buffer)?; - Wallet::initialize( - name, - settings, - mnemonic.to_seed(self.passphrase.unwrap_or_default()), - self - .timestamp - .unwrap_or(Timestamp(bitcoincore_rpc::json::Timestamp::Time(0))) - .0, - )?; - } - } + Wallet::create( + settings, + name, + mnemonic.to_seed(self.passphrase.unwrap_or_default()), + )?; Ok(None) } diff --git a/src/wallet.rs b/src/wallet.rs index 1d6c8ca839..5d668cfc27 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,32 +1,39 @@ use { + self::persister::{DatabasePersister, TransactionPersister}, super::*, batch::ParentInfo, + bdk::{keys::KeyMap, ChangeSet, KeychainKind, PersistedWallet, WalletPersister}, bitcoin::{ bip32::{ChildNumber, DerivationPath, Xpriv}, psbt::Psbt, secp256k1::Secp256k1, }, - bitcoincore_rpc::json::ImportDescriptors, entry::{EtchingEntry, EtchingEntryValue}, fee_rate::FeeRate, index::entry::Entry, indicatif::{ProgressBar, ProgressStyle}, log::log_enabled, - miniscript::descriptor::{DescriptorSecretKey, DescriptorXKey, Wildcard}, - redb::{Database, DatabaseError, ReadableTable, RepairSession, StorageError, TableDefinition}, + miniscript::descriptor::{ + Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, Wildcard, + }, + redb::{Database, ReadableTable, RepairSession, StorageError, TableDefinition, WriteTransaction}, std::sync::Once, transaction_builder::TransactionBuilder, }; pub mod batch; pub mod entry; +mod persister; pub mod transaction_builder; pub mod wallet_constructor; -const SCHEMA_VERSION: u64 = 1; +const LOOKAHEAD: u32 = 1000; +const SCHEMA_VERSION: u64 = 2; +define_table! { CHANGESET, (), &str } define_table! { RUNE_TO_ETCHING, u128, EtchingEntryValue } define_table! { STATISTICS, u64, u64 } +define_table! { XPRIV, (), [u8; 78] } #[derive(Copy, Clone)] pub(crate) enum Statistic { @@ -45,22 +52,6 @@ impl From for u64 { } } -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct Descriptor { - pub desc: String, - pub timestamp: bitcoincore_rpc::bitcoincore_rpc_json::Timestamp, - pub active: bool, - pub internal: Option, - pub range: Option<(u64, u64)>, - pub next: Option, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct ListDescriptorsResult { - pub wallet_name: String, - pub descriptors: Vec, -} - #[derive(Debug, PartialEq)] pub(crate) enum Maturity { BelowMinimumHeight(u64), @@ -71,21 +62,218 @@ pub(crate) enum Maturity { } pub(crate) struct Wallet { - bitcoin_client: Client, - database: Database, + database: Arc, has_rune_index: bool, has_sat_index: bool, - rpc_url: Url, - utxos: BTreeMap, ord_client: reqwest::blocking::Client, - inscription_info: BTreeMap, - output_info: BTreeMap, - inscriptions: BTreeMap>, - locked_utxos: BTreeMap, + rpc_url: Url, settings: Settings, + wallet: PersistedWallet, } impl Wallet { + pub(crate) fn create(settings: &Settings, name: &str, seed: [u8; 64]) -> Result { + let path = Self::database_path(settings, name); + + if path.exists() { + bail!("wallet `{}` at `{}` already exists", name, path.display()); + } + + let dir = path.parent().unwrap(); + if let Err(err) = fs::create_dir_all(dir) { + bail!("failed to create data dir `{}`: {err}", dir.display()); + } + + let database = Database::builder().create(&path)?; + + let network = settings.chain().network(); + + let master_private_key = Xpriv::new_master(network, &seed)?; + + let external = Wallet::derive_descriptor(network, master_private_key, KeychainKind::External)?; + + let internal = Wallet::derive_descriptor(network, master_private_key, KeychainKind::Internal)?; + + let mut tx = database.begin_write()?; + + tx.set_quick_repair(true); + + tx.open_table(CHANGESET)?; + + tx.open_table(RUNE_TO_ETCHING)?; + + tx.open_table(STATISTICS)? + .insert(&Statistic::Schema.key(), &SCHEMA_VERSION)?; + + tx.open_table(XPRIV)? + .insert((), master_private_key.encode())?; + + let mut persister = TransactionPersister(&mut tx); + + let mut wallet = bdk::Wallet::create(external.clone(), internal.clone()) + .network(network) + .lookahead(LOOKAHEAD) + .create_wallet(&mut persister)?; + + wallet.persist(&mut persister)?; + + tx.commit()?; + + Ok(()) + } + + fn database_path(settings: &Settings, wallet_name: &str) -> PathBuf { + settings + .data_dir() + .join("wallets") + .join(format!("{wallet_name}.redb")) + } + + pub(crate) fn open_database(settings: &Settings, wallet_name: &str) -> Result { + let path = Self::database_path(settings, wallet_name); + + let db_path = path.clone().to_owned(); + let once = Once::new(); + let progress_bar = Mutex::new(None); + let integration_test = settings.integration_test(); + + let repair_callback = move |progress: &mut RepairSession| { + once.call_once(|| { + println!( + "Wallet database file `{}` needs recovery. This can take some time.", + db_path.display() + ) + }); + + if !(cfg!(test) || log_enabled!(log::Level::Info) || integration_test) { + let mut guard = progress_bar.lock().unwrap(); + + let progress_bar = guard.get_or_insert_with(|| { + let progress_bar = ProgressBar::new(100); + progress_bar.set_style( + ProgressStyle::with_template("[repairing database] {wide_bar} {pos}/{len}").unwrap(), + ); + progress_bar + }); + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + progress_bar.set_position((progress.progress() * 100.0) as u64); + } + }; + + let database = match Database::builder() + .set_repair_callback(repair_callback) + .open(&path) + { + Ok(database) => { + { + let schema_version = database + .begin_read()? + .open_table(STATISTICS)? + .get(&Statistic::Schema.key())? + .map(|x| x.value()) + .unwrap_or(0); + + match schema_version.cmp(&SCHEMA_VERSION) { + cmp::Ordering::Less => + bail!( + "wallet database at `{}` appears to have been built with an older, incompatible version of ord, consider deleting and rebuilding the index: index schema {schema_version}, ord schema {SCHEMA_VERSION}", + path.display() + ), + cmp::Ordering::Greater => + bail!( + "wallet database at `{}` appears to have been built with a newer, incompatible version of ord, consider updating ord: index schema {schema_version}, ord schema {SCHEMA_VERSION}", + path.display() + ), + cmp::Ordering::Equal => { + } + } + } + + database + } + Err(error) => bail!("failed to open wallet database: {error}"), + }; + + Ok(database) + } + + pub(crate) fn get_descriptor( + &self, + kind: KeychainKind, + ) -> Result> { + let tx = self.database.begin_read()?; + + let master_private_key = tx + .open_table(XPRIV)? + .get(())? + .map(|xpriv| Xpriv::decode(xpriv.value().as_slice())) + .transpose()? + .ok_or(anyhow!("couldn't load master private key from database"))?; + + let (descriptor, _keymap) = + Wallet::derive_descriptor(self.settings.chain().network(), master_private_key, kind)?; + + Ok(descriptor) + } + + pub(crate) fn get_receive_addresses(&mut self, n: usize) -> Vec
{ + (0..n) + .map(|_| { + self + .wallet + .reveal_next_address(KeychainKind::External) + .address + }) + .collect() + } + + pub(crate) fn derive_descriptor( + network: Network, + master_private_key: Xpriv, + kind: KeychainKind, + ) -> Result<(Descriptor, KeyMap)> { + const ACCOUNT: u32 = 0; + + let secp = Secp256k1::new(); + + let fingerprint = master_private_key.fingerprint(&secp); + + let derivation_path = DerivationPath::master() + .child(ChildNumber::Hardened { index: 86 }) + .child(ChildNumber::Hardened { + index: u32::from(network != Network::Bitcoin), + }) + .child(ChildNumber::Hardened { index: ACCOUNT }); + + let derived_private_key = master_private_key.derive_priv(&secp, &derivation_path)?; + + let secret_key = DescriptorSecretKey::XPrv(DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: derived_private_key, + derivation_path: DerivationPath::master().child(ChildNumber::Normal { + index: (kind as u8).into(), + }), + wildcard: Wildcard::Unhardened, + }); + + let public_key = secret_key.to_public(&secp)?; + + let mut key_map = BTreeMap::new(); + key_map.insert(public_key.clone(), secret_key); + + let descriptor = Descriptor::new_tr(public_key, None)?; + + Ok((descriptor, key_map)) + } + + pub(crate) fn persist(&mut self) -> Result { + self + .wallet + .persist(&mut DatabasePersister(self.database.clone()))?; + Ok(()) + } + pub(crate) fn get_wallet_sat_ranges(&self) -> Result)>> { ensure!( self.has_sat_index, @@ -93,7 +281,7 @@ impl Wallet { ); let mut output_sat_ranges = Vec::new(); - for (output, info) in self.output_info.iter() { + for (output, info) in self.output_info().iter() { if let Some(sat_ranges) = &info.sat_ranges { output_sat_ranges.push((*output, sat_ranges.clone())); } else { @@ -110,7 +298,7 @@ impl Wallet { "ord index must be built with `--index-sats` to see sat ranges" ); - if let Some(info) = self.output_info.get(output) { + if let Some(info) = self.output_info().get(output) { if let Some(sat_ranges) = &info.sat_ranges { Ok(sat_ranges.clone()) } else { @@ -127,7 +315,7 @@ impl Wallet { "ord index must be built with `--index-sats` to use `--sat`" ); - for (outpoint, info) in self.output_info.iter() { + for (outpoint, info) in self.output_info().iter() { if let Some(sat_ranges) = &info.sat_ranges { let mut offset = 0; for (start, end) in sat_ranges { @@ -150,15 +338,15 @@ impl Wallet { } pub(crate) fn bitcoin_client(&self) -> &Client { - &self.bitcoin_client + panic!("attempt to access bitcoin client") } pub(crate) fn utxos(&self) -> &BTreeMap { - &self.utxos + unimplemented!() } pub(crate) fn locked_utxos(&self) -> &BTreeMap { - &self.locked_utxos + unimplemented!() } pub(crate) fn lock_non_cardinal_outputs(&self) -> Result { @@ -191,11 +379,15 @@ impl Wallet { } pub(crate) fn inscriptions(&self) -> &BTreeMap> { - &self.inscriptions + unimplemented!(); } pub(crate) fn inscription_info(&self) -> BTreeMap { - self.inscription_info.clone() + unimplemented!(); + } + + pub(crate) fn output_info(&self) -> BTreeMap { + unimplemented!(); } pub(crate) fn get_inscription( @@ -238,7 +430,7 @@ impl Wallet { ) -> Result>> { Ok( self - .output_info + .output_info() .get(output) .ok_or(anyhow!("output not found in wallet"))? .inscriptions @@ -254,13 +446,13 @@ impl Wallet { } let satpoint = self - .inscription_info + .inscription_info() .get(parent_id) .ok_or_else(|| anyhow!("parent {parent_id} not in wallet"))? .satpoint; let tx_out = self - .utxos + .utxos() .get(&satpoint.outpoint) .ok_or_else(|| anyhow!("parent {parent_id} not in wallet"))? .clone(); @@ -278,7 +470,7 @@ impl Wallet { pub(crate) fn get_runic_outputs(&self) -> Result>> { let mut runic_outputs = BTreeSet::new(); - for (output, info) in &self.output_info { + for (output, info) in &self.output_info() { let Some(runes) = &info.runes else { return Ok(None); }; @@ -297,7 +489,7 @@ impl Wallet { ) -> Result>> { Ok( self - .output_info + .output_info() .get(output) .ok_or(anyhow!("output not found in wallet"))? .runes @@ -333,7 +525,7 @@ impl Wallet { pub(crate) fn get_change_address(&self) -> Result
{ Ok( self - .bitcoin_client + .bitcoin_client() .call::>("getrawchangeaddress", &["bech32m".into()]) .context("could not get change addresses from wallet")? .require_network(self.chain().network())?, @@ -466,265 +658,6 @@ impl Wallet { }) } - fn check_descriptors(wallet_name: &str, descriptors: Vec) -> Result> { - let tr = descriptors - .iter() - .filter(|descriptor| descriptor.desc.starts_with("tr(")) - .count(); - - let rawtr = descriptors - .iter() - .filter(|descriptor| descriptor.desc.starts_with("rawtr(")) - .count(); - - if tr != 2 || descriptors.len() != 2 + rawtr { - bail!("wallet \"{}\" contains unexpected output descriptors, and does not appear to be an `ord` wallet, create a new wallet with `ord wallet create`", wallet_name); - } - - Ok(descriptors) - } - - pub(crate) fn initialize_from_descriptors( - name: String, - settings: &Settings, - descriptors: Vec, - ) -> Result { - let client = Self::check_version(settings.bitcoin_rpc_client(Some(name.clone()))?)?; - - let descriptors = Self::check_descriptors(&name, descriptors)?; - - client.create_wallet(&name, None, Some(true), None, None)?; - - let descriptors = descriptors - .into_iter() - .map(|descriptor| ImportDescriptors { - descriptor: descriptor.desc.clone(), - timestamp: descriptor.timestamp, - active: Some(true), - range: descriptor.range.map(|(start, end)| { - ( - usize::try_from(start).unwrap_or(0), - usize::try_from(end).unwrap_or(0), - ) - }), - next_index: descriptor - .next - .map(|next| usize::try_from(next).unwrap_or(0)), - internal: descriptor.internal, - label: None, - }) - .collect::>(); - - client.call::("importdescriptors", &[serde_json::to_value(descriptors)?])?; - - Ok(()) - } - - pub(crate) fn initialize( - name: String, - settings: &Settings, - seed: [u8; 64], - timestamp: bitcoincore_rpc::json::Timestamp, - ) -> Result { - Self::check_version(settings.bitcoin_rpc_client(None)?)?.create_wallet( - &name, - None, - Some(true), - None, - None, - )?; - - let network = settings.chain().network(); - - let secp = Secp256k1::new(); - - let master_private_key = Xpriv::new_master(network, &seed)?; - - let fingerprint = master_private_key.fingerprint(&secp); - - let derivation_path = DerivationPath::master() - .child(ChildNumber::Hardened { index: 86 }) - .child(ChildNumber::Hardened { - index: u32::from(network != Network::Bitcoin), - }) - .child(ChildNumber::Hardened { index: 0 }); - - let derived_private_key = master_private_key.derive_priv(&secp, &derivation_path)?; - - let mut descriptors = Vec::new(); - for change in [false, true] { - let secret_key = DescriptorSecretKey::XPrv(DescriptorXKey { - origin: Some((fingerprint, derivation_path.clone())), - xkey: derived_private_key, - derivation_path: DerivationPath::master().child(ChildNumber::Normal { - index: change.into(), - }), - wildcard: Wildcard::Unhardened, - }); - - let public_key = secret_key.to_public(&secp)?; - - let mut key_map = BTreeMap::new(); - key_map.insert(public_key.clone(), secret_key); - - let descriptor = miniscript::descriptor::Descriptor::new_tr(public_key, None)?; - - descriptors.push(ImportDescriptors { - descriptor: descriptor.to_string_with_secret(&key_map), - timestamp, - active: Some(true), - range: None, - next_index: None, - internal: Some(change), - label: None, - }); - } - - match settings - .bitcoin_rpc_client(Some(name.clone()))? - .call::( - "importdescriptors", - &[serde_json::to_value(descriptors.clone())?], - ) { - Ok(_) => Ok(()), - Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err))) - if err.code == -4 && err.message == "Wallet already loading." => - { - // wallet loading - Ok(()) - } - Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err))) - if err.code == -35 => - { - // wallet already loaded - Ok(()) - } - Err(err) => { - bail!("Failed to import descriptors for wallet {}: {err}", name) - } - } - } - - pub(crate) fn check_version(client: Client) -> Result { - const MIN_VERSION: usize = 280000; - - let bitcoin_version = client.version()?; - if bitcoin_version < MIN_VERSION { - bail!( - "Bitcoin Core {} or newer required, current version is {}", - Self::format_bitcoin_core_version(MIN_VERSION), - Self::format_bitcoin_core_version(bitcoin_version), - ); - } else { - Ok(client) - } - } - - fn format_bitcoin_core_version(version: usize) -> String { - format!( - "{}.{}.{}", - version / 10000, - version % 10000 / 100, - version % 100 - ) - } - - pub(crate) fn open_database(wallet_name: &String, settings: &Settings) -> Result { - let path = settings - .data_dir() - .join("wallets") - .join(format!("{wallet_name}.redb")); - - if let Err(err) = fs::create_dir_all(path.parent().unwrap()) { - bail!( - "failed to create data dir `{}`: {err}", - path.parent().unwrap().display() - ); - } - - let db_path = path.clone().to_owned(); - let once = Once::new(); - let progress_bar = Mutex::new(None); - let integration_test = settings.integration_test(); - - let repair_callback = move |progress: &mut RepairSession| { - once.call_once(|| { - println!( - "Wallet database file `{}` needs recovery. This can take some time.", - db_path.display() - ) - }); - - if !(cfg!(test) || log_enabled!(log::Level::Info) || integration_test) { - let mut guard = progress_bar.lock().unwrap(); - - let progress_bar = guard.get_or_insert_with(|| { - let progress_bar = ProgressBar::new(100); - progress_bar.set_style( - ProgressStyle::with_template("[repairing database] {wide_bar} {pos}/{len}").unwrap(), - ); - progress_bar - }); - - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - progress_bar.set_position((progress.progress() * 100.0) as u64); - } - }; - - let database = match Database::builder() - .set_repair_callback(repair_callback) - .open(&path) - { - Ok(database) => { - { - let schema_version = database - .begin_read()? - .open_table(STATISTICS)? - .get(&Statistic::Schema.key())? - .map(|x| x.value()) - .unwrap_or(0); - - match schema_version.cmp(&SCHEMA_VERSION) { - cmp::Ordering::Less => - bail!( - "wallet database at `{}` appears to have been built with an older, incompatible version of ord, consider deleting and rebuilding the index: index schema {schema_version}, ord schema {SCHEMA_VERSION}", - path.display() - ), - cmp::Ordering::Greater => - bail!( - "wallet database at `{}` appears to have been built with a newer, incompatible version of ord, consider updating ord: index schema {schema_version}, ord schema {SCHEMA_VERSION}", - path.display() - ), - cmp::Ordering::Equal => { - } - } - } - - database - } - Err(DatabaseError::Storage(StorageError::Io(error))) - if error.kind() == io::ErrorKind::NotFound => - { - let database = Database::builder().create(&path)?; - - let mut tx = database.begin_write()?; - tx.set_quick_repair(true); - - tx.open_table(RUNE_TO_ETCHING)?; - - tx.open_table(STATISTICS)? - .insert(&Statistic::Schema.key(), &SCHEMA_VERSION)?; - - tx.commit()?; - - database - } - Err(error) => bail!("failed to open wallet database: {error}"), - }; - - Ok(database) - } - pub(crate) fn save_etching( &self, rune: &Rune, diff --git a/src/wallet/persister.rs b/src/wallet/persister.rs new file mode 100644 index 0000000000..32577790fd --- /dev/null +++ b/src/wallet/persister.rs @@ -0,0 +1,54 @@ +use {super::*, bdk::chain::Merge}; + +pub(crate) struct TransactionPersister<'a>(pub(crate) &'a mut WriteTransaction); + +impl WalletPersister for TransactionPersister<'_> { + type Error = Error; + + fn initialize(persister: &mut Self) -> std::result::Result { + let changeset = match persister + .0 + .open_table(CHANGESET)? + .get(())? + .map(|result| result.value().to_string()) + { + Some(result) => serde_json::from_str::(result.as_str())?, + None => ChangeSet::default(), + }; + + Ok(changeset) + } + + fn persist(persister: &mut Self, changeset: &ChangeSet) -> std::result::Result<(), Self::Error> { + let mut current = Self::initialize(persister)?; + + current.merge(changeset.clone()); + + persister + .0 + .open_table(CHANGESET)? + .insert((), serde_json::to_string(¤t)?.as_str())?; + + Ok(()) + } +} + +pub(crate) struct DatabasePersister(pub(crate) Arc); + +impl WalletPersister for DatabasePersister { + type Error = Error; + + fn initialize(persister: &mut Self) -> std::result::Result { + TransactionPersister::initialize(&mut TransactionPersister(&mut persister.0.begin_write()?)) + } + + fn persist(persister: &mut Self, changeset: &ChangeSet) -> std::result::Result<(), Self::Error> { + let mut wtx = persister.0.begin_write()?; + + TransactionPersister::persist(&mut TransactionPersister(&mut wtx), changeset)?; + + wtx.commit()?; + + Ok(()) + } +} diff --git a/src/wallet/wallet_constructor.rs b/src/wallet/wallet_constructor.rs index 72c3b193fb..c10d3c8540 100644 --- a/src/wallet/wallet_constructor.rs +++ b/src/wallet/wallet_constructor.rs @@ -4,18 +4,12 @@ use super::*; pub(crate) struct WalletConstructor { ord_client: reqwest::blocking::Client, name: String, - no_sync: bool, rpc_url: Url, settings: Settings, } impl WalletConstructor { - pub(crate) fn construct( - name: String, - no_sync: bool, - settings: Settings, - rpc_url: Url, - ) -> Result { + pub(crate) fn construct(name: String, settings: Settings, rpc_url: Url) -> Result { let mut headers = HeaderMap::new(); headers.insert( reqwest::header::ACCEPT, @@ -36,7 +30,6 @@ impl WalletConstructor { .default_headers(headers.clone()) .build()?, name, - no_sync, rpc_url, settings, } @@ -44,202 +37,57 @@ impl WalletConstructor { } pub(crate) fn build(self) -> Result { - let database = Wallet::open_database(&self.name, &self.settings)?; - - let bitcoin_client = { - let client = - Wallet::check_version(self.settings.bitcoin_rpc_client(Some(self.name.clone()))?)?; - - if !client.list_wallets()?.contains(&self.name) { - loop { - match client.load_wallet(&self.name) { - Ok(_) => { - break; - } - Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err))) - if err.code == -4 && err.message == "Wallet already loading." => - { - // wallet loading - eprint!("."); - thread::sleep(Duration::from_secs(3)); - continue; - } - Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err))) - if err.code == -35 => - { - // wallet already loaded - break; - } - Err(err) => { - bail!("Failed to load wallet {}: {err}", self.name); - } - } - } - } - - if client.get_wallet_info()?.private_keys_enabled { - Wallet::check_descriptors( - &self.name, - client - .call::("listdescriptors", &[serde_json::Value::Null])? - .descriptors, - )?; - } - - client + let database = Arc::new(Wallet::open_database(&self.settings, &self.name)?); + + let tx = database.begin_read()?; + + let master_private_key = tx + .open_table(XPRIV)? + .get(())? + .map(|xpriv| Xpriv::decode(xpriv.value().as_slice())) + .transpose()? + .ok_or_else(|| anyhow!("failed to load master private key from database"))?; + + let wallet = match bdk::Wallet::load() + .check_network(self.settings.chain().network()) + .descriptor( + KeychainKind::External, + Some(Wallet::derive_descriptor( + self.settings.chain().network(), + master_private_key, + KeychainKind::External, + )?), + ) + .descriptor( + KeychainKind::Internal, + Some(Wallet::derive_descriptor( + self.settings.chain().network(), + master_private_key, + KeychainKind::Internal, + )?), + ) + .extract_keys() + .lookahead(LOOKAHEAD) + .load_wallet(&mut DatabasePersister(database.clone())) + .context("failed to load wallet")? + { + Some(wallet) => wallet, + None => bail!("no wallet found, create one first"), }; - let bitcoin_block_count = bitcoin_client.get_block_count().unwrap() + 1; - - if !self.no_sync { - for i in 0.. { - let ord_block_count = self.get("/blockcount")?.text()?.parse::().expect( - "wallet failed to retrieve block count from server. Make sure `ord server` is running.", - ); - - if ord_block_count >= bitcoin_block_count { - break; - } else if i == 20 { - bail!( - "`ord server` {} blocks behind `bitcoind`, consider using `--no-sync` to ignore this error", - bitcoin_block_count - ord_block_count - ); - } - std::thread::sleep(Duration::from_millis(50)); - } - } - - let mut utxos = Self::get_utxos(&bitcoin_client)?; - let locked_utxos = Self::get_locked_utxos(&bitcoin_client)?; - utxos.extend(locked_utxos.clone()); - - let output_info = self.get_output_info(utxos.clone().into_keys().collect())?; - - let inscriptions = output_info - .iter() - .flat_map(|(_output, info)| info.inscriptions.clone().unwrap_or_default()) - .collect::>(); - - let (inscriptions, inscription_info) = self.get_inscriptions(&inscriptions)?; - let status = self.get_server_status()?; Ok(Wallet { - bitcoin_client, database, has_rune_index: status.rune_index, has_sat_index: status.sat_index, - inscription_info, - inscriptions, - locked_utxos, ord_client: self.ord_client, - output_info, rpc_url: self.rpc_url, settings: self.settings, - utxos, + wallet, }) } - fn get_output_info(&self, outputs: Vec) -> Result> { - let response = self.post("/outputs", &outputs)?; - - if !response.status().is_success() { - bail!("wallet failed get outputs: {}", response.text()?); - } - - let response_outputs = serde_json::from_str::>(&response.text()?)?; - - ensure! { - response_outputs.len() == outputs.len(), - "unexpected server `/outputs` response length", - } - - let output_info: BTreeMap = - outputs.into_iter().zip(response_outputs).collect(); - - for (output, info) in &output_info { - if !info.indexed { - bail!("output in wallet but not in ord server: {output}"); - } - } - - Ok(output_info) - } - - fn get_inscriptions( - &self, - inscriptions: &Vec, - ) -> Result<( - BTreeMap>, - BTreeMap, - )> { - let response = self.post("/inscriptions", inscriptions)?; - - if !response.status().is_success() { - bail!("wallet failed get inscriptions: {}", response.text()?); - } - - let mut inscriptions = BTreeMap::new(); - let mut inscription_infos = BTreeMap::new(); - for info in serde_json::from_str::>(&response.text()?)? { - inscriptions - .entry(info.satpoint) - .or_insert_with(Vec::new) - .push(info.id); - - inscription_infos.insert(info.id, info); - } - - Ok((inscriptions, inscription_infos)) - } - - fn get_utxos(bitcoin_client: &Client) -> Result> { - Ok( - bitcoin_client - .list_unspent(None, None, None, None, None)? - .into_iter() - .map(|utxo| { - let outpoint = OutPoint::new(utxo.txid, utxo.vout); - let txout = TxOut { - script_pubkey: utxo.script_pub_key, - value: utxo.amount, - }; - - (outpoint, txout) - }) - .collect(), - ) - } - - fn get_locked_utxos(bitcoin_client: &Client) -> Result> { - #[derive(Deserialize)] - pub(crate) struct JsonOutPoint { - txid: Txid, - vout: u32, - } - - let outpoints = bitcoin_client.call::>("listlockunspent", &[])?; - - let mut utxos = BTreeMap::new(); - - for outpoint in outpoints { - let Some(tx_out) = bitcoin_client.get_tx_out(&outpoint.txid, outpoint.vout, Some(false))? - else { - continue; - }; - - utxos.insert( - OutPoint::new(outpoint.txid, outpoint.vout), - TxOut { - value: tx_out.value, - script_pubkey: ScriptBuf::from_bytes(tx_out.script_pub_key.hex), - }, - ); - } - - Ok(utxos) - } - fn get_server_status(&self) -> Result { let response = self.get("/status")?; @@ -257,14 +105,4 @@ impl WalletConstructor { .send() .map_err(|err| anyhow!(err)) } - - pub fn post(&self, path: &str, body: &impl Serialize) -> Result { - self - .ord_client - .post(self.rpc_url.join(path)?) - .json(body) - .header(reqwest::header::ACCEPT, "application/json") - .send() - .map_err(|err| anyhow!(err)) - } } diff --git a/tests/lib.rs b/tests/lib.rs index 43e351b05c..3bf607cb0e 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -14,8 +14,8 @@ use { mockcore::TransactionTemplate, ord::{ api, base64_decode, base64_encode, chain::Chain, decimal::Decimal, outgoing::Outgoing, - subcommand::runes::RuneInfo, templates::InscriptionHtml, wallet::batch, - wallet::ListDescriptorsResult, Inscription, InscriptionId, RuneEntry, + subcommand::runes::RuneInfo, templates::InscriptionHtml, wallet::batch, Inscription, + InscriptionId, RuneEntry, }, ordinals::{ Artifact, Charm, Edict, Pile, Rarity, Rune, RuneId, Runestone, Sat, SatPoint, SpacedRune, @@ -91,8 +91,7 @@ fn create_wallet(core: &mockcore::Handle, ord: &TestServer) { CommandBuilder::new(format!("--chain {} wallet create", core.network())) .core(core) .ord(ord) - .stdout_regex(".*") - .run_and_extract_stdout(); + .run_and_deserialize_output::(); } fn sats( diff --git a/tests/test_server.rs b/tests/test_server.rs index 57bdce2a8b..9bbfe6ca52 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -11,8 +11,7 @@ pub(crate) struct TestServer { bitcoin_rpc_url: String, ord_server_handle: Handle, port: u16, - #[allow(unused)] - tempdir: TempDir, + tempdir: Arc, } impl TestServer { @@ -77,10 +76,23 @@ impl TestServer { bitcoin_rpc_url: core.url(), ord_server_handle, port, - tempdir, + tempdir: tempdir.into(), } } + pub(crate) fn create_wallet(&self, core: &mockcore::Handle) { + CommandBuilder::new(format!("--chain {} wallet create", core.network())) + .temp_dir(self.tempdir.clone()) + .core(core) + .ord(self) + .stdout_regex(".*") + .run_and_extract_stdout(); + } + + pub(crate) fn tempdir(&self) -> &Arc { + &self.tempdir + } + pub(crate) fn url(&self) -> Url { format!("http://127.0.0.1:{}", self.port).parse().unwrap() } diff --git a/tests/wallet.rs b/tests/wallet.rs index c81400457a..cf329ed7c4 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -7,7 +7,6 @@ mod batch_command; mod burn; mod cardinals; mod create; -mod dump; mod inscribe; mod inscriptions; mod label; diff --git a/tests/wallet/create.rs b/tests/wallet/create.rs index f34cc1e4e9..99e2ee1401 100644 --- a/tests/wallet/create.rs +++ b/tests/wallet/create.rs @@ -1,16 +1,52 @@ -use {super::*, ord::subcommand::wallet::create::Output}; +use { + super::*, + ord::subcommand::wallet::{create::Output, descriptors::Output as Descriptors}, +}; #[test] fn create() { let core = mockcore::spawn(); - assert!(!core.wallets().contains("ord")); + let tempdir = Arc::new(TempDir::new().unwrap()); + + let wallet_db = tempdir.path().join("wallets/ord.redb"); + + assert!(!wallet_db.try_exists().unwrap()); + + CommandBuilder::new("wallet create") + .core(&core) + .temp_dir(tempdir.clone()) + .run_and_deserialize_output::(); + + assert!(wallet_db.try_exists().unwrap()); + assert!(wallet_db.is_file()); +} + +#[test] +fn create_with_same_name_fails() { + let core = mockcore::spawn(); + + let tempdir = TempDir::new().unwrap(); + + let wallet_db = tempdir.path().join("wallets/ord.redb"); + + assert!(!wallet_db.try_exists().unwrap()); + + let arc = Arc::new(tempdir); CommandBuilder::new("wallet create") .core(&core) + .temp_dir(arc.clone()) .run_and_deserialize_output::(); - assert!(core.wallets().contains("ord")); + assert!(wallet_db.try_exists().unwrap()); + + CommandBuilder::new("wallet create") + .core(&core) + .temp_dir(arc.clone()) + .expected_exit_code(1) + .stderr_regex("error: wallet `ord` at .* already exists.*") + .run_and_extract_stdout(); } #[test] @@ -25,69 +61,117 @@ fn seed_phrases_are_twelve_words_long() { #[test] fn wallet_creates_correct_mainnet_taproot_descriptor() { let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); + + let tempdir = Arc::new(TempDir::new().unwrap()); CommandBuilder::new("wallet create") + .temp_dir(tempdir.clone()) .core(&core) .run_and_deserialize_output::(); - assert_eq!(core.descriptors().len(), 2); + let descriptors = CommandBuilder::new("wallet descriptors") + .temp_dir(tempdir) + .core(&core) + .ord(&ord) + .stderr_regex(".*") + .run_and_deserialize_output::(); + assert_regex_match!( - &core.descriptors()[0], - r"tr\(\[[[:xdigit:]]{8}/86'/0'/0'\]xprv[[:alnum:]]*/0/\*\)#[[:alnum:]]{8}" + &descriptors.external, + r"tr\(\[[[:xdigit:]]{8}/86'/0'/0'\]xpub[[:alnum:]]*/0/\*\)#[[:alnum:]]{8}" ); + assert_regex_match!( - &core.descriptors()[1], - r"tr\(\[[[:xdigit:]]{8}/86'/0'/0'\]xprv[[:alnum:]]*/1/\*\)#[[:alnum:]]{8}" + &descriptors.internal, + r"tr\(\[[[:xdigit:]]{8}/86'/0'/0'\]xpub[[:alnum:]]*/1/\*\)#[[:alnum:]]{8}" ); } #[test] fn wallet_creates_correct_test_network_taproot_descriptor() { let core = mockcore::builder().network(Network::Signet).build(); + let ord = TestServer::spawn_with_args(&core, &["--signet"]); + + let tempdir = Arc::new(TempDir::new().unwrap()); CommandBuilder::new("--chain signet wallet create") + .temp_dir(tempdir.clone()) .core(&core) .run_and_deserialize_output::(); - assert_eq!(core.descriptors().len(), 2); + let descriptors = CommandBuilder::new("--chain signet wallet descriptors") + .temp_dir(tempdir) + .core(&core) + .ord(&ord) + .stderr_regex(".*") + .run_and_deserialize_output::(); + assert_regex_match!( - &core.descriptors()[0], - r"tr\(\[[[:xdigit:]]{8}/86'/1'/0'\]tprv[[:alnum:]]*/0/\*\)#[[:alnum:]]{8}" + &descriptors.external, + r"tr\(\[[[:xdigit:]]{8}/86'/1'/0'\]tpub[[:alnum:]]*/0/\*\)#[[:alnum:]]{8}" ); + assert_regex_match!( - &core.descriptors()[1], - r"tr\(\[[[:xdigit:]]{8}/86'/1'/0'\]tprv[[:alnum:]]*/1/\*\)#[[:alnum:]]{8}" + &descriptors.internal, + r"tr\(\[[[:xdigit:]]{8}/86'/1'/0'\]tpub[[:alnum:]]*/1/\*\)#[[:alnum:]]{8}" ); } #[test] -fn detect_wrong_descriptors() { +fn create_with_different_name() { let core = mockcore::spawn(); - CommandBuilder::new("wallet create") - .core(&core) - .run_and_deserialize_output::(); + let tempdir = Arc::new(TempDir::new().unwrap()); + + let name = "inscription-wallet"; - core.import_descriptor("wpkh([aslfjk])#a23ad2l".to_string()); + let database = tempdir.path().join(format!("wallets/{name}.redb")); - CommandBuilder::new("wallet transactions") + assert!(!database.try_exists().unwrap()); + + CommandBuilder::new(format!("wallet --name {name} create")) .core(&core) - .stderr_regex( - r#"error: wallet "ord" contains unexpected output descriptors, and does not appear to be an `ord` wallet, create a new wallet with `ord wallet create`\n"#, - ) - .expected_exit_code(1) - .run_and_extract_stdout(); + .temp_dir(tempdir.clone()) + .run_and_deserialize_output::(); + + assert!(database.try_exists().unwrap()); + assert!(database.is_file()); } #[test] -fn create_with_different_name() { - let core = mockcore::spawn(); +fn create_wallet_with_same_name_different_network_fails() { + let mainnet_core = mockcore::spawn(); + let signet_core = mockcore::builder().network(Network::Signet).build(); - assert!(!core.wallets().contains("inscription-wallet")); + let tempdir = Arc::new(TempDir::new().unwrap()); + let mainnet_database = tempdir.path().join("wallets/ord.redb"); + let signet_database = tempdir.path().join("signet/wallets/ord.redb"); - CommandBuilder::new("wallet --name inscription-wallet create") - .core(&core) + assert!(!mainnet_database.try_exists().unwrap()); + + CommandBuilder::new("wallet create") + .core(&mainnet_core) + .temp_dir(tempdir.clone()) .run_and_deserialize_output::(); - assert!(core.wallets().contains("inscription-wallet")); + assert!(mainnet_database.try_exists().unwrap()); + + fs::create_dir_all(signet_database.parent().unwrap()).unwrap(); + fs::rename(&mainnet_database, &signet_database).unwrap(); + + CommandBuilder::new("--chain signet wallet descriptors") + .core(&signet_core) + .temp_dir(tempdir.clone()) + .expected_exit_code(1) + .expected_stderr( + "error: failed to load wallet + +because: +- data mismatch: Network { loaded: Bitcoin, expected: Signet } +", + ) + .run_and_extract_stdout(); + + assert!(signet_database.try_exists().unwrap()); } diff --git a/tests/wallet/dump.rs b/tests/wallet/dump.rs deleted file mode 100644 index cdae859cfd..0000000000 --- a/tests/wallet/dump.rs +++ /dev/null @@ -1,77 +0,0 @@ -use super::*; - -#[test] -fn dumped_descriptors_match_wallet_descriptors() { - let core = mockcore::spawn(); - let ord = TestServer::spawn(&core); - - create_wallet(&core, &ord); - - let output = CommandBuilder::new("wallet dump") - .core(&core) - .ord(&ord) - .stderr_regex(".*") - .run_and_deserialize_output::(); - - assert!(core - .descriptors() - .iter() - .zip(output.descriptors.iter()) - .all(|(wallet_descriptor, output_descriptor)| *wallet_descriptor == output_descriptor.desc)); -} - -#[test] -fn dumped_descriptors_restore() { - let core = mockcore::spawn(); - let ord = TestServer::spawn(&core); - - create_wallet(&core, &ord); - - let output = CommandBuilder::new("wallet dump") - .core(&core) - .ord(&ord) - .stderr_regex(".*") - .run_and_deserialize_output::(); - - let core = mockcore::spawn(); - - CommandBuilder::new("wallet restore --from descriptor") - .stdin(serde_json::to_string(&output).unwrap().as_bytes().to_vec()) - .core(&core) - .ord(&ord) - .run_and_extract_stdout(); - - assert!(core - .descriptors() - .iter() - .zip(output.descriptors.iter()) - .all(|(wallet_descriptor, output_descriptor)| *wallet_descriptor == output_descriptor.desc)); -} - -#[test] -fn dump_and_restore_descriptors_with_minify() { - let core = mockcore::spawn(); - let ord = TestServer::spawn(&core); - - create_wallet(&core, &ord); - - let output = CommandBuilder::new("--format minify wallet dump") - .core(&core) - .ord(&ord) - .stderr_regex(".*") - .run_and_deserialize_output::(); - - let core = mockcore::spawn(); - - CommandBuilder::new("wallet restore --from descriptor") - .stdin(serde_json::to_string(&output).unwrap().as_bytes().to_vec()) - .core(&core) - .ord(&ord) - .run_and_extract_stdout(); - - assert!(core - .descriptors() - .iter() - .zip(output.descriptors.iter()) - .all(|(wallet_descriptor, output_descriptor)| *wallet_descriptor == output_descriptor.desc)); -} diff --git a/tests/wallet/receive.rs b/tests/wallet/receive.rs index 210e32de57..d0ae6fdbdc 100644 --- a/tests/wallet/receive.rs +++ b/tests/wallet/receive.rs @@ -5,16 +5,25 @@ fn receive() { let core = mockcore::spawn(); let ord = TestServer::spawn(&core); - create_wallet(&core, &ord); + ord.create_wallet(&core); let output = CommandBuilder::new("wallet receive") + .temp_dir(ord.tempdir().clone()) .core(&core) .ord(&ord) .run_and_deserialize_output::(); - assert!(output - .addresses - .first() - .unwrap() - .is_valid_for_network(Network::Bitcoin)); + let first_address = output.addresses.first().unwrap(); + + assert!(first_address.is_valid_for_network(Network::Bitcoin)); + + let output = CommandBuilder::new("wallet receive") + .temp_dir(ord.tempdir().clone()) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + let second_address = output.addresses.first().unwrap(); + + assert!(second_address != first_address); } diff --git a/tests/wallet/restore.rs b/tests/wallet/restore.rs index abf8c8c968..ed2c8f4991 100644 --- a/tests/wallet/restore.rs +++ b/tests/wallet/restore.rs @@ -1,4 +1,7 @@ -use {super::*, ord::subcommand::wallet::create}; +use { + super::*, + ord::subcommand::wallet::{create, descriptors::Output as Descriptors}, +}; #[test] fn restore_generates_same_descriptors() { @@ -7,47 +10,41 @@ fn restore_generates_same_descriptors() { let ord = TestServer::spawn(&core); + let tempdir = Arc::new(TempDir::new().unwrap()); + let create::Output { mnemonic, .. } = CommandBuilder::new("wallet create") + .temp_dir(tempdir.clone()) .core(&core) .run_and_deserialize_output(); - let output = CommandBuilder::new("wallet dump") + let descriptors = CommandBuilder::new("wallet descriptors") + .temp_dir(tempdir) .core(&core) .ord(&ord) - .stderr_regex(".*THIS STRING CONTAINS YOUR PRIVATE KEYS.*") - .run_and_deserialize_output::(); - - // new descriptors are created with timestamp `now` - assert!(output - .descriptors - .iter() - .all(|descriptor| descriptor.timestamp == bitcoincore_rpc::json::Timestamp::Now)); + .run_and_deserialize_output::(); - (mnemonic, core.descriptors()) + (mnemonic, descriptors) }; let core = mockcore::spawn(); - CommandBuilder::new(["wallet", "restore", "--from", "mnemonic"]) + let tempdir = Arc::new(TempDir::new().unwrap()); + + CommandBuilder::new(["wallet", "restore"]) + .temp_dir(tempdir.clone()) .stdin(mnemonic.to_string().into()) .core(&core) .run_and_extract_stdout(); let ord = TestServer::spawn(&core); - let output = CommandBuilder::new("wallet dump") + let restored_descriptors = CommandBuilder::new("wallet descriptors") + .temp_dir(tempdir.clone()) .core(&core) .ord(&ord) - .stderr_regex(".*THIS STRING CONTAINS YOUR PRIVATE KEYS.*") - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); - // restored descriptors are created with timestamp `0` - assert!(output - .descriptors - .iter() - .all(|descriptor| descriptor.timestamp == bitcoincore_rpc::json::Timestamp::Time(0))); - - assert_eq!(core.descriptors(), descriptors); + assert_eq!(descriptors, restored_descriptors); } #[test] @@ -55,128 +52,67 @@ fn restore_generates_same_descriptors_with_passphrase() { let passphrase = "foo"; let (mnemonic, descriptors) = { let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); + + let tempdir = Arc::new(TempDir::new().unwrap()); let create::Output { mnemonic, .. } = CommandBuilder::new(["wallet", "create", "--passphrase", passphrase]) + .temp_dir(tempdir.clone()) + .ord(&ord) .core(&core) .run_and_deserialize_output(); - (mnemonic, core.descriptors()) - }; - - let core = mockcore::spawn(); - - CommandBuilder::new([ - "wallet", - "restore", - "--passphrase", - passphrase, - "--from", - "mnemonic", - ]) - .stdin(mnemonic.to_string().into()) - .core(&core) - .run_and_extract_stdout(); + let output = CommandBuilder::new("wallet descriptors") + .temp_dir(tempdir) + .core(&core) + .ord(&ord) + .stderr_regex(".*") + .run_and_deserialize_output::(); - assert_eq!(core.descriptors(), descriptors); -} + (mnemonic, output) + }; -#[test] -fn restore_to_existing_wallet_fails() { let core = mockcore::spawn(); let ord = TestServer::spawn(&core); - create_wallet(&core, &ord); + let tempdir = Arc::new(TempDir::new().unwrap()); - let descriptors = core.descriptors(); - - let output = CommandBuilder::new("wallet dump") + CommandBuilder::new(["wallet", "restore", "--passphrase", passphrase]) + .temp_dir(tempdir.clone()) + .stdin(mnemonic.to_string().into()) .core(&core) .ord(&ord) - .stderr_regex(".*") - .run_and_deserialize_output::(); + .run_and_extract_stdout(); - CommandBuilder::new("wallet restore --from descriptor") - .stdin(serde_json::to_string(&output).unwrap().as_bytes().to_vec()) + let output = CommandBuilder::new("wallet descriptors") + .temp_dir(tempdir) .core(&core) .ord(&ord) - .expected_exit_code(1) - .expected_stderr("error: wallet `ord` already exists\n") - .run_and_extract_stdout(); + .stderr_regex(".*") + .run_and_deserialize_output::(); - assert_eq!( - descriptors, - output - .descriptors - .into_iter() - .map(|descriptor| descriptor.desc) - .collect::>() - ); + assert_eq!(output, descriptors); } #[test] -fn restore_with_wrong_descriptors_fails() { - let core = mockcore::spawn(); +fn restore_to_existing_wallet_fails() { + let tempdir = Arc::new(TempDir::new().unwrap()); - CommandBuilder::new("wallet --name foo restore --from descriptor") - .stdin(r#" -{ - "wallet_name": "bar", - "descriptors": [ - { - "desc": "rawtr(cVMYXp8uf1yFU9AAY6NJu1twA2uT94mHQBGkfgqCCzp6RqiTWCvP)#tah5crv7", - "timestamp": 1706047934, - "active": false, - "internal": null, - "range": null, - "next": null - }, - { - "desc": "rawtr(cVdVu6VRwYXsTPMiptqVYLcp7EtQi5sjxLzbPTSNwW6CkCxBbEFs)#5afaht8d", - "timestamp": 1706047934, - "active": false, - "internal": null, - "range": null, - "next": null - }, - { - "desc": "wpkh([c0b9536d/86'/1'/0']tprv8fXhtVjj3vb7kgxKuiWXzcUsur44gbLbbtwxL4HKmpzkBNoMrYqbQhMe7MWhrZjLFc9RBpTRYZZkrS8HH1Q3SmD5DkfpjKqtd97q1JWfqzr/0/*)#dweuu0ww", - "timestamp": 1706047839, - "active": true, - "internal": false, - "range": [ - 0, - 1000 - ], - "next": 1 - }, - { - "desc": "tr([c0b9536d/86'/1'/0']tprv8fXhtVjj3vb7kgxKuiWXzcUsur44gbLbbtwxL4HKmpzkBNoMrYqbQhMe7MWhrZjLFc9RBpTRYZZkrS8HH1Q3SmD5DkfpjKqtd97q1JWfqzr/1/*)#u6uap67k", - "timestamp": 1706047839, - "active": true, - "internal": true, - "range": [ - 0, - 1013 - ], - "next": 14 - } - ] -}"#.into()) - .core(&core) - .expected_exit_code(1) - .expected_stderr("error: wallet \"foo\" contains unexpected output descriptors, and does not appear to be an `ord` wallet, create a new wallet with `ord wallet create`\n") - .run_and_extract_stdout(); -} + let create::Output { mnemonic, .. } = CommandBuilder::new("wallet create") + .temp_dir(tempdir.clone()) + .run_and_deserialize_output(); -#[test] -fn restore_with_compact_works() { let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - CommandBuilder::new("wallet restore --from descriptor") - .stdin(r#"{"wallet_name":"foo","descriptors":[{"desc":"rawtr(cVMYXp8uf1yFU9AAY6NJu1twA2uT94mHQBGkfgqCCzp6RqiTWCvP)#tah5crv7","timestamp":1706047934,"active":false,"internal":null,"range":null,"next":null},{"desc":"rawtr(cVdVu6VRwYXsTPMiptqVYLcp7EtQi5sjxLzbPTSNwW6CkCxBbEFs)#5afaht8d","timestamp":1706047934,"active":false,"internal":null,"range":null,"next":null},{"desc":"tr([c0b9536d/86'/1'/0']tprv8fXhtVjj3vb7kgxKuiWXzcUsur44gbLbbtwxL4HKmpzkBNoMrYqbQhMe7MWhrZjLFc9RBpTRYZZkrS8HH1Q3SmD5DkfpjKqtd97q1JWfqzr/0/*)#dweuu0ww","timestamp":1706047839,"active":true,"internal":false,"range":[0,1000],"next":1},{"desc":"tr([c0b9536d/86'/1'/0']tprv8fXhtVjj3vb7kgxKuiWXzcUsur44gbLbbtwxL4HKmpzkBNoMrYqbQhMe7MWhrZjLFc9RBpTRYZZkrS8HH1Q3SmD5DkfpjKqtd97q1JWfqzr/1/*)#u6uap67k","timestamp":1706047839,"active":true,"internal":true,"range":[0,1013],"next":14}]}"#.into()) + CommandBuilder::new("wallet restore") + .temp_dir(tempdir) + .stdin(mnemonic.to_string().into()) .core(&core) - .expected_exit_code(0) + .ord(&ord) + .expected_exit_code(1) + .stderr_regex("error: wallet `ord` at .* already exists\n") .run_and_extract_stdout(); } @@ -194,177 +130,10 @@ fn restore_with_blank_mnemonic_generates_same_descriptors() { let core = mockcore::spawn(); - CommandBuilder::new(["wallet", "restore", "--from", "mnemonic"]) + CommandBuilder::new(["wallet", "restore"]) .stdin(mnemonic.to_string().into()) .core(&core) .run_and_extract_stdout(); assert_eq!(core.descriptors(), descriptors); } - -#[test] -fn passphrase_conflicts_with_descriptor() { - let core = mockcore::spawn(); - let ord = TestServer::spawn(&core); - - CommandBuilder::new([ - "wallet", - "restore", - "--from", - "descriptor", - "--passphrase", - "supersecurepassword", - ]) - .stdin("".into()) - .core(&core) - .ord(&ord) - .expected_exit_code(1) - .expected_stderr("error: descriptor does not take a passphrase\n") - .run_and_extract_stdout(); -} - -#[test] -fn timestamp_conflicts_with_descriptor() { - let core = mockcore::spawn(); - let ord = TestServer::spawn(&core); - - CommandBuilder::new([ - "wallet", - "restore", - "--from", - "descriptor", - "--timestamp", - "now", - ]) - .stdin("".into()) - .core(&core) - .ord(&ord) - .expected_exit_code(1) - .expected_stderr("error: descriptor does not take a timestamp\n") - .run_and_extract_stdout(); -} - -#[test] -fn restore_with_now_timestamp() { - let mnemonic = { - let core = mockcore::spawn(); - - let create::Output { mnemonic, .. } = CommandBuilder::new(["wallet", "create"]) - .core(&core) - .run_and_deserialize_output(); - - mnemonic - }; - - let core = mockcore::spawn(); - let ord = TestServer::spawn(&core); - - CommandBuilder::new([ - "wallet", - "restore", - "--from", - "mnemonic", - "--timestamp", - "now", - ]) - .stdin(mnemonic.to_string().into()) - .core(&core) - .run_and_extract_stdout(); - - let output = CommandBuilder::new("wallet dump") - .core(&core) - .ord(&ord) - .stderr_regex(".*") - .run_and_deserialize_output::(); - - assert!(output - .descriptors - .iter() - .all(|descriptor| match descriptor.timestamp { - bitcoincore_rpc::json::Timestamp::Now => true, - bitcoincore_rpc::json::Timestamp::Time(time) => - time.abs_diff( - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - ) <= 5, - })); -} - -#[test] -fn restore_with_no_timestamp_defaults_to_0() { - let mnemonic = { - let core = mockcore::spawn(); - - let create::Output { mnemonic, .. } = CommandBuilder::new(["wallet", "create"]) - .core(&core) - .run_and_deserialize_output(); - - mnemonic - }; - - let core = mockcore::spawn(); - let ord = TestServer::spawn(&core); - - CommandBuilder::new(["wallet", "restore", "--from", "mnemonic"]) - .stdin(mnemonic.to_string().into()) - .core(&core) - .run_and_extract_stdout(); - - let output = CommandBuilder::new("wallet dump") - .core(&core) - .ord(&ord) - .stderr_regex(".*") - .run_and_deserialize_output::(); - - assert!(output - .descriptors - .iter() - .all(|descriptor| match descriptor.timestamp { - bitcoincore_rpc::json::Timestamp::Now => false, - bitcoincore_rpc::json::Timestamp::Time(time) => time == 0, - })); -} - -#[test] -fn restore_with_timestamp() { - let mnemonic = { - let core = mockcore::spawn(); - - let create::Output { mnemonic, .. } = CommandBuilder::new(["wallet", "create"]) - .core(&core) - .run_and_deserialize_output(); - - mnemonic - }; - - let core = mockcore::spawn(); - let ord = TestServer::spawn(&core); - - CommandBuilder::new([ - "wallet", - "restore", - "--from", - "mnemonic", - "--timestamp", - "123456789", - ]) - .stdin(mnemonic.to_string().into()) - .core(&core) - .run_and_extract_stdout(); - - let output = CommandBuilder::new("wallet dump") - .core(&core) - .ord(&ord) - .stderr_regex(".*") - .run_and_deserialize_output::(); - - assert!(output - .descriptors - .iter() - .all(|descriptor| match descriptor.timestamp { - bitcoincore_rpc::json::Timestamp::Now => false, - bitcoincore_rpc::json::Timestamp::Time(time) => time == 123456789, - })); -} From d401a376bd69e5d4aeadd0ee4f3e2e9482b95983 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Tue, 4 Mar 2025 14:52:20 +0100 Subject: [PATCH 2/6] Always store script pubkey in output table --- src/index.rs | 2 +- src/index/updater.rs | 12 +++--------- src/index/utxo_entry.rs | 38 +++++++++++++------------------------- 3 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/index.rs b/src/index.rs index 641ce9085b..ff794afb59 100644 --- a/src/index.rs +++ b/src/index.rs @@ -50,7 +50,7 @@ mod utxo_entry; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 30; +const SCHEMA_VERSION: u64 = 31; define_multimap_table! { SAT_TO_SEQUENCE_NUMBER, u64, u32 } define_multimap_table! { SEQUENCE_NUMBER_TO_CHILDREN, u32, u32 } diff --git a/src/index/updater.rs b/src/index/updater.rs index 5eb2c0abc7..44f18081be 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -574,9 +574,7 @@ impl Updater<'_> { let mut entry = UtxoEntryBuf::new(); entry.push_value(txout.value.to_sat(), self.index); - if self.index.index_addresses { - entry.push_script_pubkey(txout.script_pubkey.as_bytes(), self.index); - } + entry.push_script_pubkey(txout.script_pubkey.as_bytes(), self.index); entry }; @@ -632,9 +630,7 @@ impl Updater<'_> { } } - if self.index.index_addresses { - self.index_transaction_output_script_pubkeys(tx, &mut output_utxo_entries); - } + self.index_transaction_output_script_pubkeys(tx, &mut output_utxo_entries); if index_inscriptions { inscription_updater.index_inscriptions( @@ -685,9 +681,7 @@ impl Updater<'_> { let mut new_utxo_entry = UtxoEntryBuf::new(); new_utxo_entry.push_sat_ranges(&lost_sat_ranges, self.index); - if self.index.index_addresses { - new_utxo_entry.push_script_pubkey(&[], self.index); - } + new_utxo_entry.push_script_pubkey(&[], self.index); *utxo_entry = UtxoEntryBuf::merged(utxo_entry, &new_utxo_entry, self.index); } diff --git a/src/index/utxo_entry.rs b/src/index/utxo_entry.rs index e80437da2a..b49914faa1 100644 --- a/src/index/utxo_entry.rs +++ b/src/index/utxo_entry.rs @@ -21,13 +21,12 @@ enum Sats<'a> { /// by that many 11-byte sat range entries, otherwise the total output value /// stored as a varint. /// -/// If `--index-addresses`, the script pubkey stored as a varint followed by -/// that many bytes of data. -/// /// If `--index-inscriptions`, the list of inscriptions stored as /// `(sequence_number, offset)`, with the sequence number stored as a u32 and /// the offset as a varint. /// +/// The script pubkey stored as a varint followed by that many bytes of data. +/// /// Note that the list of inscriptions doesn't need an explicit length, it /// continues until the end of the array. /// @@ -43,7 +42,7 @@ pub struct UtxoEntry { impl UtxoEntry { pub fn parse(&self, index: &Index) -> ParsedUtxoEntry { let sats; - let mut script_pubkey = None; + let script_pubkey; let mut inscriptions = None; let mut offset = 0; @@ -61,14 +60,12 @@ impl UtxoEntry { offset += varint_len; }; - if index.index_addresses { - let (script_pubkey_len, varint_len) = varint::decode(&self.bytes[offset..]).unwrap(); - offset += varint_len; + let (script_pubkey_len, varint_len) = varint::decode(&self.bytes[offset..]).unwrap(); + offset += varint_len; - let script_pubkey_len: usize = script_pubkey_len.try_into().unwrap(); - script_pubkey = Some(&self.bytes[offset..offset + script_pubkey_len]); - offset += script_pubkey_len; - } + let script_pubkey_len: usize = script_pubkey_len.try_into().unwrap(); + script_pubkey = Some(&self.bytes[offset..offset + script_pubkey_len]); + offset += script_pubkey_len; if index.index_inscriptions { inscriptions = Some(&self.bytes[offset..self.bytes.len()]); @@ -229,7 +226,6 @@ impl UtxoEntryBuf { } pub fn push_script_pubkey(&mut self, script_pubkey: &[u8], index: &Index) { - assert!(index.index_addresses); varint::encode_to_vec(script_pubkey.len().try_into().unwrap(), &mut self.vec); self.vec.extend(script_pubkey); @@ -255,13 +251,9 @@ impl UtxoEntryBuf { } #[cfg(debug_assertions)] - fn advance_state(&mut self, expected_state: State, new_state: State, index: &Index) { + fn advance_state(&mut self, expected_state: State, new_state: State, _index: &Index) { assert!(self.state == expected_state); self.state = new_state; - - if self.state == State::NeedScriptPubkey && !index.index_addresses { - self.state = State::Valid; - } } pub fn merged(a: &UtxoEntry, b: &UtxoEntry, index: &Index) -> Self { @@ -278,11 +270,9 @@ impl UtxoEntryBuf { merged.push_value(0, index); } - if index.index_addresses { - assert!(a_parsed.script_pubkey().is_empty()); - assert!(b_parsed.script_pubkey().is_empty()); - merged.push_script_pubkey(&[], index); - } + assert!(a_parsed.script_pubkey().is_empty()); + assert!(b_parsed.script_pubkey().is_empty()); + merged.push_script_pubkey(&[], index); if index.index_inscriptions { merged.push_inscriptions(a_parsed.inscriptions(), index); @@ -301,9 +291,7 @@ impl UtxoEntryBuf { utxo_entry.push_value(0, index); } - if index.index_addresses { - utxo_entry.push_script_pubkey(&[], index); - } + utxo_entry.push_script_pubkey(&[], index); utxo_entry } From 12574a751a82464bb54a5b4a54f125d12bebe944 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 4 Mar 2025 08:09:04 -0800 Subject: [PATCH 3/6] Remove unnecessary forward declaration --- src/index/utxo_entry.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/index/utxo_entry.rs b/src/index/utxo_entry.rs index b49914faa1..b2bdb5076b 100644 --- a/src/index/utxo_entry.rs +++ b/src/index/utxo_entry.rs @@ -42,7 +42,6 @@ pub struct UtxoEntry { impl UtxoEntry { pub fn parse(&self, index: &Index) -> ParsedUtxoEntry { let sats; - let script_pubkey; let mut inscriptions = None; let mut offset = 0; @@ -63,8 +62,8 @@ impl UtxoEntry { let (script_pubkey_len, varint_len) = varint::decode(&self.bytes[offset..]).unwrap(); offset += varint_len; - let script_pubkey_len: usize = script_pubkey_len.try_into().unwrap(); - script_pubkey = Some(&self.bytes[offset..offset + script_pubkey_len]); + let script_pubkey_len = usize::try_from(script_pubkey_len).unwrap(); + let script_pubkey = Some(&self.bytes[offset..offset + script_pubkey_len]); offset += script_pubkey_len; if index.index_inscriptions { From 516c94f3e0ac57deed2075abcc39548f75722846 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Thu, 6 Mar 2025 00:33:25 +0100 Subject: [PATCH 4/6] Remove index --- src/index/updater.rs | 6 +++--- src/index/utxo_entry.rs | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/index/updater.rs b/src/index/updater.rs index 44f18081be..e417fbafbc 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -574,7 +574,7 @@ impl Updater<'_> { let mut entry = UtxoEntryBuf::new(); entry.push_value(txout.value.to_sat(), self.index); - entry.push_script_pubkey(txout.script_pubkey.as_bytes(), self.index); + entry.push_script_pubkey(txout.script_pubkey.as_bytes()); entry }; @@ -681,7 +681,7 @@ impl Updater<'_> { let mut new_utxo_entry = UtxoEntryBuf::new(); new_utxo_entry.push_sat_ranges(&lost_sat_ranges, self.index); - new_utxo_entry.push_script_pubkey(&[], self.index); + new_utxo_entry.push_script_pubkey(&[]); *utxo_entry = UtxoEntryBuf::merged(utxo_entry, &new_utxo_entry, self.index); } @@ -719,7 +719,7 @@ impl Updater<'_> { output_utxo_entries: &mut [UtxoEntryBuf], ) { for (vout, txout) in tx.output.iter().enumerate() { - output_utxo_entries[vout].push_script_pubkey(txout.script_pubkey.as_bytes(), self.index); + output_utxo_entries[vout].push_script_pubkey(txout.script_pubkey.as_bytes()); } } diff --git a/src/index/utxo_entry.rs b/src/index/utxo_entry.rs index b2bdb5076b..e237551bd4 100644 --- a/src/index/utxo_entry.rs +++ b/src/index/utxo_entry.rs @@ -210,7 +210,7 @@ impl UtxoEntryBuf { varint::encode_to_vec(value.into(), &mut self.vec); #[cfg(debug_assertions)] - self.advance_state(State::NeedSats, State::NeedScriptPubkey, index); + self.advance_state(State::NeedSats, State::NeedScriptPubkey); } pub fn push_sat_ranges(&mut self, sat_ranges: &[u8], index: &Index) { @@ -221,15 +221,15 @@ impl UtxoEntryBuf { self.vec.extend(sat_ranges); #[cfg(debug_assertions)] - self.advance_state(State::NeedSats, State::NeedScriptPubkey, index); + self.advance_state(State::NeedSats, State::NeedScriptPubkey); } - pub fn push_script_pubkey(&mut self, script_pubkey: &[u8], index: &Index) { + pub fn push_script_pubkey(&mut self, script_pubkey: &[u8]) { varint::encode_to_vec(script_pubkey.len().try_into().unwrap(), &mut self.vec); self.vec.extend(script_pubkey); #[cfg(debug_assertions)] - self.advance_state(State::NeedScriptPubkey, State::Valid, index); + self.advance_state(State::NeedScriptPubkey, State::Valid); } pub fn push_inscriptions(&mut self, inscriptions: &[u8], index: &Index) { @@ -237,7 +237,7 @@ impl UtxoEntryBuf { self.vec.extend(inscriptions); #[cfg(debug_assertions)] - self.advance_state(State::Valid, State::Valid, index); + self.advance_state(State::Valid, State::Valid); } pub fn push_inscription(&mut self, sequence_number: u32, satpoint_offset: u64, index: &Index) { @@ -246,11 +246,11 @@ impl UtxoEntryBuf { varint::encode_to_vec(satpoint_offset.into(), &mut self.vec); #[cfg(debug_assertions)] - self.advance_state(State::Valid, State::Valid, index); + self.advance_state(State::Valid, State::Valid); } #[cfg(debug_assertions)] - fn advance_state(&mut self, expected_state: State, new_state: State, _index: &Index) { + fn advance_state(&mut self, expected_state: State, new_state: State) { assert!(self.state == expected_state); self.state = new_state; } @@ -271,7 +271,7 @@ impl UtxoEntryBuf { assert!(a_parsed.script_pubkey().is_empty()); assert!(b_parsed.script_pubkey().is_empty()); - merged.push_script_pubkey(&[], index); + merged.push_script_pubkey(&[]); if index.index_inscriptions { merged.push_inscriptions(a_parsed.inscriptions(), index); @@ -290,7 +290,7 @@ impl UtxoEntryBuf { utxo_entry.push_value(0, index); } - utxo_entry.push_script_pubkey(&[], index); + utxo_entry.push_script_pubkey(&[]); utxo_entry } From e2ec8bb49e472e43666399b63fdcbb4694050730 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Thu, 6 Mar 2025 01:17:29 +0100 Subject: [PATCH 5/6] Add test --- src/index.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/index.rs b/src/index.rs index ff794afb59..d577fc3473 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2455,12 +2455,56 @@ impl Index { txout, ))) } + + #[cfg(test)] + pub(crate) fn list_all_spks(&self) -> Result> { + let rtx = self.database.begin_read()?; + let mut spks = Vec::new(); + + for entry in rtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?.iter()? { + let (_outpoint, utxo_entry) = entry?; + + spks.push(ScriptBuf::from_bytes( + utxo_entry.value().parse(self).script_pubkey().to_vec(), + )); + } + + Ok(spks) + } } #[cfg(test)] mod tests { use {super::*, crate::index::testing::Context}; + #[test] + fn list_all_spks() { + let context = Context::builder().build(); + + context.mine_blocks(2); + + let tx_1 = TransactionTemplate { + inputs: &[(1, 0, 0, Default::default())], + fee: 0, + recipient: Some(address(0)), + ..default() + }; + + let tx_2 = TransactionTemplate { + inputs: &[(2, 0, 0, Default::default())], + fee: 0, + recipient: Some(address(1)), + ..default() + }; + + context.core.broadcast_tx(tx_1); + context.core.broadcast_tx(tx_2); + + context.mine_blocks(1); + + assert_eq!(context.index.list_all_spks().unwrap().len(), 4); + } + #[test] fn height_limit() { { From 5be353a194b6a0acd83505419e1088c5e9a1eaa1 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Thu, 6 Mar 2025 02:21:35 +0100 Subject: [PATCH 6/6] Amend --- src/index/utxo_entry.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index/utxo_entry.rs b/src/index/utxo_entry.rs index e237551bd4..ad74944447 100644 --- a/src/index/utxo_entry.rs +++ b/src/index/utxo_entry.rs @@ -21,12 +21,12 @@ enum Sats<'a> { /// by that many 11-byte sat range entries, otherwise the total output value /// stored as a varint. /// +/// The script pubkey stored as a varint followed by that many bytes of data. +/// /// If `--index-inscriptions`, the list of inscriptions stored as /// `(sequence_number, offset)`, with the sequence number stored as a u32 and /// the offset as a varint. /// -/// The script pubkey stored as a varint followed by that many bytes of data. -/// /// Note that the list of inscriptions doesn't need an explicit length, it /// continues until the end of the array. ///