diff --git a/node/src/bin/space-cli.rs b/node/src/bin/space-cli.rs index c928654..6e71d7d 100644 --- a/node/src/bin/space-cli.rs +++ b/node/src/bin/space-cli.rs @@ -1,6 +1,6 @@ extern crate core; -use std::{fs, path::PathBuf}; +use std::{fs, path::PathBuf, str::FromStr}; use base64::{prelude::BASE64_STANDARD, Engine}; use clap::{Parser, Subcommand}; @@ -10,7 +10,9 @@ use jsonrpsee::{ }; use protocol::{ bitcoin::{Amount, FeeRate, OutPoint, Txid}, + hasher::{KeyHasher, SpaceHash}, opcodes::OP_SETALL, + sname::{NameLike, SName}, Covenant, FullSpaceOut, }; use serde::{Deserialize, Serialize}; @@ -20,6 +22,7 @@ use spaced::{ BidParams, ExecuteParams, OpenParams, RegisterParams, RpcClient, RpcWalletRequest, RpcWalletTxBuilder, SendCoinsParams, TransferSpacesParams, }, + store::Sha256, wallets::AddressKind, }; @@ -222,6 +225,12 @@ enum Commands { /// compatible with most bitcoin wallets #[command(name = "getnewaddress")] GetCoinAddress, + /// Calculate a spacehash from the specified space name + #[command(name = "spacehash")] + SpaceHash { + /// The space name + space: String, + }, } struct SpaceCli { @@ -264,7 +273,7 @@ impl SpaceCli { let result = self .client .wallet_send_request( - self.wallet.clone(), + &self.wallet, RpcWalletTxBuilder { auction_outputs, requests: match req { @@ -360,6 +369,13 @@ async fn main() -> anyhow::Result<()> { Ok(()) } +fn space_hash(spaceish: &str) -> anyhow::Result { + let space = normalize_space(&spaceish); + let sname = SName::from_str(&space)?; + let spacehash = SpaceHash::from(Sha256::hash(sname.to_bytes())); + Ok(hex::encode(spacehash.as_slice())) +} + async fn handle_commands( cli: &SpaceCli, command: Commands, @@ -373,7 +389,7 @@ async fn handle_commands( for (priority, spacehash) in hashes { let outpoint = cli .client - .get_space_owner(hex::encode(spacehash.as_slice())) + .get_space_owner(&hex::encode(spacehash.as_slice())) .await?; if let Some(outpoint) = outpoint { @@ -404,8 +420,8 @@ async fn handle_commands( println!("{} sat", Amount::from_sat(response).to_string()); } Commands::GetSpace { space } => { - let space = normalize_space(&space); - let response = cli.client.get_space(space).await?; + let space_hash = space_hash(&space).map_err(|e| ClientError::Custom(e.to_string()))?; + let response = cli.client.get_space(&space_hash).await?; println!("{}", serde_json::to_string_pretty(&response)?); } Commands::GetSpaceOut { outpoint } => { @@ -413,22 +429,22 @@ async fn handle_commands( println!("{}", serde_json::to_string_pretty(&response)?); } Commands::CreateWallet { name } => { - cli.client.wallet_create(name).await?; + cli.client.wallet_create(&name).await?; } Commands::LoadWallet { name } => { - cli.client.wallet_load(name).await?; + cli.client.wallet_load(&name).await?; } Commands::ImportWallet { path } => { let content = fs::read_to_string(path).map_err(|e| ClientError::Custom(e.to_string()))?; - cli.client.wallet_import(content).await?; + cli.client.wallet_import(&content).await?; } Commands::ExportWallet { name } => { - let result = cli.client.wallet_export(name).await?; + let result = cli.client.wallet_export(&name).await?; println!("{}", result); } Commands::GetWalletInfo { name } => { - let result = cli.client.wallet_get_info(name).await?; + let result = cli.client.wallet_get_info(&name).await?; println!("{}", serde_json::to_string_pretty(&result).expect("result")); } Commands::GetServerInfo => { @@ -543,35 +559,32 @@ async fn handle_commands( .await?; } Commands::ListUnspent => { - let spaces = cli.client.wallet_list_unspent(cli.wallet.clone()).await?; + let spaces = cli.client.wallet_list_unspent(&cli.wallet).await?; println!("{}", serde_json::to_string_pretty(&spaces)?); } Commands::ListAuctionOutputs => { - let spaces = cli - .client - .wallet_list_auction_outputs(cli.wallet.clone()) - .await?; + let spaces = cli.client.wallet_list_auction_outputs(&cli.wallet).await?; println!("{}", serde_json::to_string_pretty(&spaces)?); } Commands::ListSpaces => { - let spaces = cli.client.wallet_list_spaces(cli.wallet.clone()).await?; + let spaces = cli.client.wallet_list_spaces(&cli.wallet).await?; println!("{}", serde_json::to_string_pretty(&spaces)?); } Commands::Balance => { - let balance = cli.client.wallet_get_balance(cli.wallet.clone()).await?; + let balance = cli.client.wallet_get_balance(&cli.wallet).await?; println!("{}", serde_json::to_string_pretty(&balance)?); } Commands::GetCoinAddress => { let response = cli .client - .wallet_get_new_address(cli.wallet.clone(), AddressKind::Coin) + .wallet_get_new_address(&cli.wallet, AddressKind::Coin) .await?; println!("{}", response); } Commands::GetSpaceAddress => { let response = cli .client - .wallet_get_new_address(cli.wallet.clone(), AddressKind::Space) + .wallet_get_new_address(&cli.wallet, AddressKind::Space) .await?; println!("{}", response); } @@ -579,10 +592,16 @@ async fn handle_commands( let fee_rate = FeeRate::from_sat_per_vb(fee_rate).expect("valid fee rate"); let response = cli .client - .wallet_bump_fee(cli.wallet.clone(), txid, fee_rate) + .wallet_bump_fee(&cli.wallet, txid, fee_rate) .await?; println!("{}", serde_json::to_string_pretty(&response)?); } + Commands::SpaceHash { space } => { + println!( + "{}", + space_hash(&space).map_err(|e| ClientError::Custom(e.to_string()))? + ); + } } Ok(()) diff --git a/node/src/node.rs b/node/src/node.rs index 7889589..9a5d69b 100644 --- a/node/src/node.rs +++ b/node/src/node.rs @@ -26,6 +26,7 @@ pub trait BlockSource { fn get_block(&self, hash: &BlockHash) -> Result; fn get_median_time(&self) -> Result; fn get_block_count(&self) -> Result; + fn get_best_chain(&self) -> Result; } #[derive(Debug, Clone)] diff --git a/node/src/rpc.rs b/node/src/rpc.rs index e5243cd..ef81e15 100644 --- a/node/src/rpc.rs +++ b/node/src/rpc.rs @@ -23,9 +23,8 @@ use protocol::{ OutPoint, }, constants::ChainAnchor, - hasher::{BaseHash, KeyHasher, SpaceHash}, + hasher::{BaseHash, SpaceHash}, prepare::DataSource, - sname::{NameLike, SName}, FullSpaceOut, SpaceOut, }; use serde::{Deserialize, Serialize}; @@ -43,7 +42,7 @@ use crate::{ config::ExtendedNetwork, node::ValidatedBlock, source::BitcoinRpc, - store::{ChainState, LiveSnapshot, Sha256}, + store::{ChainState, LiveSnapshot}, wallets::{AddressKind, JointBalance, RpcWallet, TxResponse, WalletCommand, WalletResponse}, }; @@ -97,10 +96,11 @@ pub trait Rpc { async fn get_server_info(&self) -> Result; #[method(name = "getspace")] - async fn get_space(&self, space: String) -> Result, ErrorObjectOwned>; + async fn get_space(&self, space_hash: &str) -> Result, ErrorObjectOwned>; #[method(name = "getspaceowner")] - async fn get_space_owner(&self, space: String) -> Result, ErrorObjectOwned>; + async fn get_space_owner(&self, space_hash: &str) + -> Result, ErrorObjectOwned>; #[method(name = "getspaceout")] async fn get_spaceout(&self, outpoint: OutPoint) -> Result, ErrorObjectOwned>; @@ -118,62 +118,58 @@ pub trait Rpc { ) -> Result, ErrorObjectOwned>; #[method(name = "walletload")] - async fn wallet_load(&self, name: String) -> Result<(), ErrorObjectOwned>; + async fn wallet_load(&self, name: &str) -> Result<(), ErrorObjectOwned>; #[method(name = "walletimport")] - async fn wallet_import(&self, content: String) -> Result<(), ErrorObjectOwned>; + async fn wallet_import(&self, content: &str) -> Result<(), ErrorObjectOwned>; #[method(name = "walletgetinfo")] - async fn wallet_get_info(&self, name: String) -> Result; + async fn wallet_get_info(&self, name: &str) -> Result; #[method(name = "walletexport")] - async fn wallet_export(&self, name: String) -> Result; + async fn wallet_export(&self, name: &str) -> Result; #[method(name = "walletcreate")] - async fn wallet_create(&self, name: String) -> Result<(), ErrorObjectOwned>; + async fn wallet_create(&self, name: &str) -> Result<(), ErrorObjectOwned>; #[method(name = "walletsendrequest")] async fn wallet_send_request( &self, - wallet: String, + wallet: &str, request: RpcWalletTxBuilder, ) -> Result; #[method(name = "walletgetnewaddress")] async fn wallet_get_new_address( &self, - wallet: String, + wallet: &str, kind: AddressKind, ) -> Result; #[method(name = "walletbumpfee")] async fn wallet_bump_fee( &self, - wallet: String, + wallet: &str, txid: Txid, fee_rate: FeeRate, ) -> Result, ErrorObjectOwned>; #[method(name = "walletlistspaces")] - async fn wallet_list_spaces( - &self, - wallet: String, - ) -> Result, ErrorObjectOwned>; + async fn wallet_list_spaces(&self, wallet: &str) + -> Result, ErrorObjectOwned>; #[method(name = "walletlistunspent")] - async fn wallet_list_unspent( - &self, - wallet: String, - ) -> Result, ErrorObjectOwned>; + async fn wallet_list_unspent(&self, wallet: &str) + -> Result, ErrorObjectOwned>; #[method(name = "walletlistauctionoutputs")] async fn wallet_list_auction_outputs( &self, - wallet: String, + wallet: &str, ) -> Result, ErrorObjectOwned>; #[method(name = "walletgetbalance")] - async fn wallet_get_balance(&self, wallet: String) -> Result; + async fn wallet_get_balance(&self, wallet: &str) -> Result; } #[derive(Clone, Serialize, Deserialize)] @@ -296,29 +292,25 @@ impl WalletManager { let mut file = fs::File::create(wallet_export_path)?; file.write_all(wallet.to_string().as_bytes())?; - self.load_wallet(client, wallet.label).await?; + self.load_wallet(client, &wallet.label).await?; Ok(()) } - pub async fn export_wallet(&self, name: String) -> anyhow::Result { - let wallet_dir = self.data_dir.join(&name); + pub async fn export_wallet(&self, name: &str) -> anyhow::Result { + let wallet_dir = self.data_dir.join(name); if !wallet_dir.exists() { return Err(anyhow!("Wallet does not exist")); } Ok(fs::read_to_string(wallet_dir.join("wallet.json"))?) } - pub async fn create_wallet( - &self, - client: &reqwest::Client, - name: String, - ) -> 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"))?; let start_block = self.get_wallet_start_block(client).await?; - self.setup_new_wallet(name.clone(), mnemonic.to_string(), start_block)?; + self.setup_new_wallet(name.to_string(), mnemonic.to_string(), start_block)?; self.load_wallet(client, name).await?; Ok(()) } @@ -440,11 +432,11 @@ impl WalletManager { Ok(true) } - pub async fn load_wallet(&self, client: &reqwest::Client, name: String) -> anyhow::Result<()> { - let wallet_dir = self.data_dir.join(name.clone()); + pub async fn load_wallet(&self, client: &reqwest::Client, name: &str) -> anyhow::Result<()> { + let wallet_dir = self.data_dir.join(name); if !wallet_dir.exists() { if self - .migrate_legacy_v0_0_1_wallet(name.clone(), wallet_dir.clone()) + .migrate_legacy_v0_0_1_wallet(name.to_string(), wallet_dir.clone()) .await? { info!("Migrated legacy wallet {}", name); @@ -461,7 +453,7 @@ impl WalletManager { let mut wallet = SpacesWallet::new(WalletConfig { start_block: export.block_height, data_dir: wallet_dir, - name: name.clone(), + name: name.to_string(), network, genesis_hash, coins_descriptors: export.descriptors(), @@ -482,7 +474,7 @@ impl WalletManager { self.wallet_loader.send(loaded_wallet).await?; let mut wallets = self.wallets.write().await; - wallets.insert(name, rpc_wallet); + wallets.insert(name.to_string(), rpc_wallet); Ok(()) } @@ -611,16 +603,8 @@ impl RpcServer for RpcServerImpl { Ok(ServerInfo { chain, tip }) } - async fn get_space(&self, space: String) -> Result, ErrorObjectOwned> { - let space = SName::from_str(&space).map_err(|_| { - ErrorObjectOwned::owned( - -1, - "must be a valid canonical space name (e.g. @bitcoin)", - None::, - ) - })?; - - let space_hash = SpaceHash::from(Sha256::hash(space.to_bytes())); + async fn get_space(&self, space_hash: &str) -> Result, ErrorObjectOwned> { + let space_hash = space_hash_from_string(space_hash)?; let info = self .store .get_space(space_hash) @@ -629,16 +613,11 @@ impl RpcServer for RpcServerImpl { Ok(info) } - async fn get_space_owner(&self, space: String) -> Result, ErrorObjectOwned> { - let space = SName::from_str(&space).map_err(|_| { - ErrorObjectOwned::owned( - -1, - "must be a valid canonical space name (e.g. @bitcoin)", - None::, - ) - })?; - let space_hash = SpaceHash::from(Sha256::hash(space.to_bytes())); - + async fn get_space_owner( + &self, + space_hash: &str, + ) -> Result, ErrorObjectOwned> { + let space_hash = space_hash_from_string(space_hash)?; let info = self .store .get_space_outpoint(space_hash) @@ -688,7 +667,7 @@ impl RpcServer for RpcServerImpl { Ok(data) } - async fn wallet_load(&self, name: String) -> Result<(), ErrorObjectOwned> { + async fn wallet_load(&self, name: &str) -> Result<(), ErrorObjectOwned> { self.wallet_manager .load_wallet(&self.client, name) .await @@ -697,16 +676,16 @@ impl RpcServer for RpcServerImpl { }) } - async fn wallet_import(&self, content: String) -> Result<(), ErrorObjectOwned> { + async fn wallet_import(&self, content: &str) -> Result<(), ErrorObjectOwned> { self.wallet_manager - .import_wallet(&self.client, &content) + .import_wallet(&self.client, content) .await .map_err(|error| { ErrorObjectOwned::owned(RPC_WALLET_NOT_LOADED, error.to_string(), None::) }) } - async fn wallet_get_info(&self, wallet: String) -> Result { + async fn wallet_get_info(&self, wallet: &str) -> Result { self.wallet(&wallet) .await? .send_get_info() @@ -714,7 +693,7 @@ impl RpcServer for RpcServerImpl { .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } - async fn wallet_export(&self, name: String) -> Result { + async fn wallet_export(&self, name: &str) -> Result { self.wallet_manager .export_wallet(name) .await @@ -723,9 +702,9 @@ impl RpcServer for RpcServerImpl { }) } - async fn wallet_create(&self, name: String) -> Result<(), ErrorObjectOwned> { + async fn wallet_create(&self, name: &str) -> Result<(), ErrorObjectOwned> { self.wallet_manager - .create_wallet(&self.client, name.clone()) + .create_wallet(&self.client, name) .await .map_err(|error| { ErrorObjectOwned::owned(RPC_WALLET_NOT_LOADED, error.to_string(), None::) @@ -733,7 +712,7 @@ impl RpcServer for RpcServerImpl { } async fn wallet_send_request( &self, - wallet: String, + wallet: &str, request: RpcWalletTxBuilder, ) -> Result { let result = self @@ -747,7 +726,7 @@ impl RpcServer for RpcServerImpl { async fn wallet_get_new_address( &self, - wallet: String, + wallet: &str, kind: AddressKind, ) -> Result { self.wallet(&wallet) @@ -759,7 +738,7 @@ impl RpcServer for RpcServerImpl { async fn wallet_bump_fee( &self, - wallet: String, + wallet: &str, txid: Txid, fee_rate: FeeRate, ) -> Result, ErrorObjectOwned> { @@ -772,7 +751,7 @@ impl RpcServer for RpcServerImpl { async fn wallet_list_spaces( &self, - wallet: String, + wallet: &str, ) -> Result, ErrorObjectOwned> { self.wallet(&wallet) .await? @@ -783,7 +762,7 @@ impl RpcServer for RpcServerImpl { async fn wallet_list_unspent( &self, - wallet: String, + wallet: &str, ) -> Result, ErrorObjectOwned> { self.wallet(&wallet) .await? @@ -794,7 +773,7 @@ impl RpcServer for RpcServerImpl { async fn wallet_list_auction_outputs( &self, - wallet: String, + wallet: &str, ) -> Result, ErrorObjectOwned> { self.wallet(&wallet) .await? @@ -803,7 +782,7 @@ impl RpcServer for RpcServerImpl { .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } - async fn wallet_get_balance(&self, wallet: String) -> Result { + async fn wallet_get_balance(&self, wallet: &str) -> Result { self.wallet(&wallet) .await? .send_get_balance() @@ -944,3 +923,21 @@ impl AsyncChainState { resp_rx.await? } } + +fn space_hash_from_string(space_hash: &str) -> Result { + let mut hash = [0u8; 32]; + hex::decode_to_slice(space_hash, &mut hash).map_err(|_| { + ErrorObjectOwned::owned( + -1, + "expected a 32-byte hex encoded space hash", + None::, + ) + })?; + SpaceHash::from_raw(hash).map_err(|_| { + ErrorObjectOwned::owned( + -1, + "expected a 32-byte hex encoded space hash", + None::, + ) + }) +} diff --git a/node/src/source.rs b/node/src/source.rs index eeb682d..d171ba9 100644 --- a/node/src/source.rs +++ b/node/src/source.rs @@ -320,6 +320,29 @@ impl BlockFetcher { self.job_id.fetch_add(1, Ordering::SeqCst); } + fn should_sync( + source: &BitcoinBlockSource, + start: ChainAnchor, + ) -> Result, BlockFetchError> { + let tip = source.get_best_chain()?; + if start.height > tip.height { + return Err(BlockFetchError::BlockMismatch); + } + + // Ensure start block is still in the best chain + let start_hash: BlockHash = source.get_block_hash(start.height)?; + if start.hash != start_hash { + return Err(BlockFetchError::BlockMismatch); + } + + // If the chain didn't advance and the hashes match, no rescan is needed + if tip.height == start.height && tip.hash == start.hash { + return Ok(None); + } + + Ok(Some(tip)) + } + pub fn start(&self, mut checkpoint: ChainAnchor) { self.stop(); @@ -343,27 +366,28 @@ impl BlockFetcher { } last_check = Instant::now(); - let tip: u32 = match task_src.get_block_count() { - Ok(t) => t as _, + let tip = match BlockFetcher::should_sync(&task_src, checkpoint) { + Ok(t) => t, Err(e) => { - _ = task_sender.send(BlockEvent::Error(BlockFetchError::RpcError(e))); + _ = task_sender.send(BlockEvent::Error(e)); return; } }; - if tip > checkpoint.height { + if let Some(tip) = tip { let res = Self::run_workers( job_id, current_task.clone(), task_src.clone(), task_sender.clone(), checkpoint, - tip, + tip.height, num_workers, ); match res { Ok(new_tip) => { + info!("new tip set: {}", new_tip.hash); checkpoint = new_tip; } Err(e) => { @@ -724,4 +748,22 @@ impl BlockSource for BitcoinBlockSource { .rpc .send_json_blocking(&self.client, &self.rpc.get_block_count())?) } + + fn get_best_chain(&self) -> Result { + #[derive(Deserialize)] + struct Info { + #[serde(rename = "blocks")] + height: u64, + #[serde(rename = "bestblockhash")] + hash: BlockHash, + } + let info: Info = self + .rpc + .send_json_blocking(&self.client, &self.rpc.get_blockchain_info())?; + + Ok(ChainAnchor { + hash: info.hash, + height: info.height as _, + }) + } } diff --git a/node/src/wallets.rs b/node/src/wallets.rs index a904ffd..fc53aea 100644 --- a/node/src/wallets.rs +++ b/node/src/wallets.rs @@ -269,7 +269,7 @@ impl RpcWallet { _ = resp.send(balance); } WalletCommand::UnloadWallet => { - info!("Unloading wallet '{}' ...", wallet.name); + info!("Unloading wallet '{}' ...", wallet.name()); } } Ok(()) diff --git a/node/tests/fetcher_tests.rs b/node/tests/fetcher_tests.rs index a6fab56..4c508e7 100644 --- a/node/tests/fetcher_tests.rs +++ b/node/tests/fetcher_tests.rs @@ -26,10 +26,7 @@ fn test_block_fetching_from_bitcoin_rpc() -> Result<()> { &rig.bitcoind.rpc_url(), BitcoinRpcAuth::UserPass("user".to_string(), "password".to_string()), )); - let (fetcher, receiver) = BlockFetcher::new( - fetcher_rpc.clone(), - 8 - ); + let (fetcher, receiver) = BlockFetcher::new(fetcher_rpc.clone(), 8); fetcher.start(ChainAnchor { hash, height: 0 }); let timeout = Duration::from_secs(5); diff --git a/node/tests/reorg_tests.rs b/node/tests/reorg_tests.rs new file mode 100644 index 0000000..1b24f48 --- /dev/null +++ b/node/tests/reorg_tests.rs @@ -0,0 +1,22 @@ +use std::time::Duration; + +use spaced::rpc::RpcClient; +use testutil::TestRig; + +#[tokio::test] +async fn it_should_resync_after_reorg_at_same_height() -> anyhow::Result<()> { + let rig = TestRig::new().await?; + rig.mine_blocks(38, None).await?; + rig.wait_until_synced().await?; + + let info = rig.spaced.client.get_server_info().await?; + assert_eq!(info.tip.height, 38); + assert_eq!(info.tip.hash, rig.get_best_block_hash().await?); + + let reorged = rig.reorg(1).await?; + assert_eq!(reorged.len(), 1); + + rig.wait_until_tip(reorged[0], Duration::from_secs(2)) + .await?; + Ok(()) +} diff --git a/node/tests/wallet_tests.rs b/node/tests/wallet_tests.rs index 42ec681..7f9b66d 100644 --- a/node/tests/wallet_tests.rs +++ b/node/tests/wallet_tests.rs @@ -12,14 +12,14 @@ async fn setup() -> anyhow::Result { } async fn it_should_create_and_fund_wallet(rig: &TestRig) -> anyhow::Result<()> { - let name = "example".to_string(); - rig.spaced.client.wallet_create(name.clone()).await?; + let name = "example"; + rig.spaced.client.wallet_create(name).await?; // get an address from the wallet to fund it let addr = Address::from_str( &rig.spaced .client - .wallet_get_new_address(name.clone(), AddressKind::Coin) + .wallet_get_new_address(name, AddressKind::Coin) .await?, )? .assume_checked(); @@ -28,9 +28,9 @@ async fn it_should_create_and_fund_wallet(rig: &TestRig) -> anyhow::Result<()> { // mine the transaction rig.mine_blocks(1, None).await?; // wait for the wallet to sync - rig.wait_until_wallet_synced(&name).await?; + rig.wait_until_wallet_synced(name).await?; - let balance = rig.spaced.client.wallet_get_balance(name.clone()).await?; + let balance = rig.spaced.client.wallet_get_balance(name).await?; assert_eq!( balance.confirmed.total, Amount::from_sat(1000_000), @@ -51,10 +51,10 @@ async fn it_should_handle_simple_reorg(rig: &TestRig) -> anyhow::Result<()> { rig.mine_empty_block().await?; assert_eq!(103, rig.get_block_count().await?); - let name = "example".to_string(); + let name = "example"; rig.wait_until_wallet_synced(&name).await?; - let balance = rig.spaced.client.wallet_get_balance(name.clone()).await?; + let balance = rig.spaced.client.wallet_get_balance(name).await?; assert_eq!( balance.confirmed.total, Amount::from_sat(0), @@ -65,7 +65,7 @@ async fn it_should_handle_simple_reorg(rig: &TestRig) -> anyhow::Result<()> { rig.mine_blocks(1, None).await?; rig.wait_until_wallet_synced(&name).await?; - let balance = rig.spaced.client.wallet_get_balance(name.clone()).await?; + let balance = rig.spaced.client.wallet_get_balance(name).await?; assert_eq!( balance.confirmed.total, Amount::from_sat(1000_000), diff --git a/testutil/src/lib.rs b/testutil/src/lib.rs index f7ae472..81b1931 100644 --- a/testutil/src/lib.rs +++ b/testutil/src/lib.rs @@ -19,6 +19,7 @@ use ::spaced::{ use anyhow::Result; use bitcoind::{ anyhow, + anyhow::anyhow, bitcoincore_rpc::{ bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules}, RpcApi, @@ -90,6 +91,22 @@ impl TestRig { } } + /// Waits until spaced tip == specified best_hash or specified time out + pub async fn wait_until_tip(&self, tip: BlockHash, timeout: Duration) -> Result<()> { + let start_time = tokio::time::Instant::now(); + + loop { + let info = self.spaced.client.get_server_info().await?; + if info.tip.hash == tip { + return Ok(()); + } + if start_time.elapsed() >= timeout { + return Err(anyhow!("Rimed out waiting for tip {:?}", tip)); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + /// Waits until named wallet tip == bitcoind tip pub async fn wait_until_wallet_synced(&self, wallet_name: &str) -> anyhow::Result<()> { loop { @@ -98,11 +115,7 @@ impl TestRig { .await .expect("handle")? as u32; - let info = self - .spaced - .client - .wallet_get_info(wallet_name.to_string()) - .await?; + let info = self.spaced.client.wallet_get_info(wallet_name).await?; if count == info.tip { return Ok(()); } diff --git a/wallet/src/builder.rs b/wallet/src/builder.rs index 13b201b..715bc64 100644 --- a/wallet/src/builder.rs +++ b/wallet/src/builder.rs @@ -460,7 +460,7 @@ impl Iterator for BuilderIterator<'_> { let mut amounts = Vec::with_capacity(params.opens.len()); for req in params.opens { - let tap = Builder::create_open_tap_data(self.wallet.network, &req.name) + let tap = Builder::create_open_tap_data(self.wallet.config.network, &req.name) .context("could not initialize tap data for name"); if tap.is_err() { return Some(Err(tap.unwrap_err())); @@ -472,7 +472,7 @@ impl Iterator for BuilderIterator<'_> { let mut contexts = Vec::with_capacity(params.executes.len()); for execute in params.executes { let signing_info = SpaceScriptSigningInfo::new( - self.wallet.network, + self.wallet.config.network, execute.script.to_nop_script(), ); if signing_info.is_err() { diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 5022f4f..35c580b 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -52,10 +52,7 @@ const WALLET_SPACE_MAGIC: &[u8; 12] = b"WALLET_SPACE"; const WALLET_COIN_MAGIC: &[u8; 12] = b"WALLET_COINS"; pub struct SpacesWallet { - pub name: String, - pub data_dir: PathBuf, - pub start_block: u32, - pub network: Network, + pub config: WalletConfig, pub coins: bdk_wallet::wallet::Wallet, pub spaces: bdk_wallet::wallet::Wallet, pub coins_db: bdk_file_store::Store, @@ -169,12 +166,12 @@ impl WalletExport { impl SpacesWallet { pub fn name(&self) -> &str { - &self.name + &self.config.name } pub fn new(config: WalletConfig) -> anyhow::Result { if !config.data_dir.exists() { - std::fs::create_dir_all(config.data_dir.clone())?; + fs::create_dir_all(config.data_dir.clone())?; } let spaces_path = config.data_dir.join("spaces.db"); @@ -212,10 +209,7 @@ impl SpacesWallet { )?; let wallet = Self { - name: config.name, - start_block: config.start_block, - data_dir: config.data_dir, - network: config.network, + config, coins: coins_wallet, spaces: spaces_wallet, coins_db, @@ -226,6 +220,15 @@ impl SpacesWallet { Ok(wallet) } + pub fn rebuild(self) -> anyhow::Result { + let config = self.config; + drop(self.spaces_db); + drop(self.coins_db); + fs::remove_file(config.data_dir.join("spaces.db"))?; + fs::remove_file(config.data_dir.join("coins.db"))?; + Ok(SpacesWallet::new(config)?) + } + pub fn get_info(&self) -> WalletInfo { let mut descriptors = Vec::with_capacity(4); @@ -263,8 +266,8 @@ impl SpacesWallet { }); WalletInfo { - label: self.name.clone(), - start_block: self.start_block, + label: self.config.name.clone(), + start_block: self.config.start_block, tip: self.coins.local_chain().tip().height(), descriptors, } @@ -297,8 +300,8 @@ impl SpacesWallet { WalletExport { descriptor, spaces_descriptor, - block_height: self.start_block, - label: self.name.clone(), + block_height: self.config.start_block, + label: self.config.name.clone(), } } @@ -603,14 +606,14 @@ impl SpacesWallet { } fn get_signing_info(&self, script: &ScriptBuf) -> Option> { - let script_info_dir = self.data_dir.join("script_solutions"); + let script_info_dir = self.config.data_dir.join("script_solutions"); let filename = hex::encode(script.as_bytes()); let file_path = script_info_dir.join(filename); std::fs::read(file_path).ok() } fn save_signing_info(&self, script: ScriptBuf, raw: Vec) -> anyhow::Result<()> { - let script_info_dir = self.data_dir.join("script_solutions"); + let script_info_dir = self.config.data_dir.join("script_solutions"); std::fs::create_dir_all(&script_info_dir) .context("could not create script_info directory")?; let filename = hex::encode(script.as_bytes()); @@ -620,7 +623,7 @@ impl SpacesWallet { } fn clear_unused_signing_info(&self) { - let script_info_dir = self.data_dir.join("script_solutions"); + let script_info_dir = self.config.data_dir.join("script_solutions"); let one_week_ago = SystemTime::now() - Duration::from_secs(7 * 24 * 60 * 60); let entries = match fs::read_dir(&script_info_dir) {