diff --git a/client/src/auth.rs b/client/src/auth.rs index fce5833..6d48bb0 100644 --- a/client/src/auth.rs +++ b/client/src/auth.rs @@ -110,5 +110,8 @@ pub fn http_client_with_auth(url: &str, auth_token: &str) -> Result Result<(), Client .await?; print_list_transactions(txs, cli.format); } - Commands::ListSpaces => { + Commands::ListSpaces { v2 } => { let tip = cli.client.get_server_info().await?; - let spaces = cli.client.wallet_list_spaces(&cli.wallet).await?; + let spaces = cli.client.wallet_list_spaces(&cli.wallet, Some(v2)).await?; print_list_spaces_response(tip.tip.height, spaces, cli.format); } Commands::Balance => { diff --git a/client/src/rpc.rs b/client/src/rpc.rs index a3be3fa..f42fbaf 100644 --- a/client/src/rpc.rs +++ b/client/src/rpc.rs @@ -327,6 +327,7 @@ pub trait Rpc { async fn wallet_list_spaces( &self, wallet: &str, + v2: Option, ) -> Result; #[method(name = "walletlistunspent")] @@ -1089,10 +1090,11 @@ impl RpcServer for RpcServerImpl { async fn wallet_list_spaces( &self, wallet: &str, + v2: Option, ) -> Result { self.wallet(&wallet) .await? - .send_list_spaces() + .send_list_spaces(v2.unwrap_or(false)) .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } diff --git a/client/src/wallets.rs b/client/src/wallets.rs index e17391c..158bbe2 100644 --- a/client/src/wallets.rs +++ b/client/src/wallets.rs @@ -1,5 +1,5 @@ use std::{collections::BTreeMap, str::FromStr, time::Duration}; - +use std::collections::{HashMap, HashSet}; use anyhow::anyhow; use clap::ValueEnum; use futures::{stream::FuturesUnordered, StreamExt}; @@ -181,6 +181,7 @@ pub enum WalletCommand { resp: crate::rpc::Responder>>, }, ListSpaces { + v2: bool, resp: crate::rpc::Responder>, }, Buy { @@ -477,8 +478,12 @@ impl RpcWallet { let transactions = Self::list_transactions(wallet, count, skip); _ = resp.send(transactions); } - WalletCommand::ListSpaces { resp } => { - let result = Self::list_spaces(wallet, state); + WalletCommand::ListSpaces { v2, resp } => { + let result = if v2 { + Self::list_spaces_v2(wallet, state) + } else { + Self::list_spaces(wallet, state) + }; _ = resp.send(result); } WalletCommand::ListBidouts { resp } => { @@ -810,6 +815,112 @@ impl RpcWallet { Ok(()) } + fn list_spaces_v2( + wallet: &mut SpacesWallet, + chain: &mut LiveSnapshot, + ) -> anyhow::Result { + let fn_start = std::time::Instant::now(); + + let unspent = wallet.list_unspent_with_details(chain)?; + log::info!("list_spaces: list_unspent_with_details took {:?} ({} results)", fn_start.elapsed(), unspent.len()); + + let t = std::time::Instant::now(); + let owned_spaces: HashSet<_> = unspent + .iter() + .filter_map(|out| out.space.as_ref().map(|s| s.name.to_string())) + .collect(); + log::info!("list_spaces: owned_spaces collect took {:?} ({} spaces)", t.elapsed(), owned_spaces.len()); + + let t = std::time::Instant::now(); + let mut recent_events: HashMap> = HashMap::new(); + for (txid, event) in wallet.list_recent_events()? { + if !event.space.as_ref().is_some_and(|s| owned_spaces.contains(s)) { + recent_events.entry(txid).or_default().push(event); + } + } + let event_count: usize = recent_events.values().map(|v| v.len()).sum(); + log::info!("list_spaces: list_recent_events + filter took {:?} ({} txids, {} events)", t.elapsed(), recent_events.len(), event_count); + + let t = std::time::Instant::now(); + let mut recent_events_with_txs = Vec::new(); + for tx in wallet.transactions() { + let Some(events) = recent_events.remove(&tx.tx_node.txid) else { + continue; + }; + for event in events { + recent_events_with_txs.push((Some(tx.clone()), event)); + } + if recent_events.is_empty() { + break; + } + } + recent_events_with_txs + .extend(recent_events.into_values().flatten().map(|e| (None, e))); + log::info!("list_spaces: transactions join took {:?} ({} matched events)", t.elapsed(), recent_events_with_txs.len()); + + let t = std::time::Instant::now(); + let mut pending = vec![]; + let mut outbid = vec![]; + let mut chain_lookups = 0u32; + for (tx, event) in recent_events_with_txs { + let name = SLabel::from_str(event.space.as_ref().unwrap()).expect("valid space name"); + if tx.as_ref() + .is_some_and(|tx| !tx.chain_position.is_confirmed()) { + pending.push(name); + continue; + } + let spacehash = SpaceKey::from(Sha256::hash(name.as_ref())); + chain_lookups += 1; + let space = chain.get_space_info(&spacehash)?; + if let Some(space) = space { + if space.spaceout.space.as_ref().unwrap().is_owned() { + continue; + } + if tx.is_none() { + outbid.push(space); + continue; + } + if event.previous_spaceout + .is_some_and(|input| input == space.outpoint()) { + continue; + } + outbid.push(space); + } + } + log::info!("list_spaces: outbid/pending classification took {:?} ({} chain lookups, {} pending, {} outbid)", t.elapsed(), chain_lookups, pending.len(), outbid.len()); + + let t = std::time::Instant::now(); + let mut owned = vec![]; + let mut winning = vec![]; + for wallet_output in unspent.into_iter().filter(|output| output.space.is_some()) { + let entry = FullSpaceOut { + txid: wallet_output.output.outpoint.txid, + spaceout: SpaceOut { + n: wallet_output.output.outpoint.vout as _, + space: wallet_output.space, + script_pubkey: wallet_output.output.txout.script_pubkey, + value: wallet_output.output.txout.value, + }, + }; + + if entry.spaceout.space.as_ref().expect("space").is_owned() { + owned.push(entry); + } else { + winning.push(entry); + } + } + log::info!("list_spaces: owned/winning split took {:?} ({} owned, {} winning)", t.elapsed(), owned.len(), winning.len()); + + log::info!("list_spaces: total elapsed {:?}", fn_start.elapsed()); + + Ok(ListSpacesResponse { + pending, + winning, + outbid, + owned, + }) + } + fn list_spaces( wallet: &mut SpacesWallet, state: &mut LiveSnapshot, @@ -1503,9 +1614,9 @@ impl RpcWallet { resp_rx.await? } - pub async fn send_list_spaces(&self) -> anyhow::Result { + pub async fn send_list_spaces(&self, v2: bool) -> anyhow::Result { let (resp, resp_rx) = oneshot::channel(); - self.sender.send(WalletCommand::ListSpaces { resp }).await?; + self.sender.send(WalletCommand::ListSpaces { v2, resp }).await?; resp_rx.await? } diff --git a/client/tests/integration_tests.rs b/client/tests/integration_tests.rs index e14fa27..62c5358 100644 --- a/client/tests/integration_tests.rs +++ b/client/tests/integration_tests.rs @@ -44,12 +44,12 @@ async fn it_should_open_a_space_for_auction(rig: &TestRig) -> anyhow::Result<()> assert!(tx_res.error.is_none(), "expect no errors for simple open"); } assert_eq!(response.result.len(), 2, "must be 2 transactions"); - let alices_spaces = rig.spaced.client.wallet_list_spaces(ALICE).await?; + let alices_spaces = rig.spaced.client.wallet_list_spaces(ALICE, None).await?; assert!(alices_spaces.pending.first().is_some_and(|s| s.to_string() == TEST_SPACE), "must be a pending space"); rig.mine_blocks(1, None).await?; rig.wait_until_synced().await?; - let alices_spaces = rig.spaced.client.wallet_list_spaces(ALICE).await?; + let alices_spaces = rig.spaced.client.wallet_list_spaces(ALICE, None).await?; assert!(alices_spaces.pending.is_empty(), "must have no pending spaces"); let fullspaceout = rig.spaced.client.get_space(TEST_SPACE).await?; @@ -83,8 +83,8 @@ async fn it_should_allow_outbidding(rig: &TestRig) -> anyhow::Result<()> { rig.wait_until_synced().await?; rig.wait_until_wallet_synced(BOB).await?; rig.wait_until_wallet_synced(ALICE).await?; - let bobs_spaces = rig.spaced.client.wallet_list_spaces(BOB).await?; - let alices_spaces = rig.spaced.client.wallet_list_spaces(ALICE).await?; + let bobs_spaces = rig.spaced.client.wallet_list_spaces(BOB, None).await?; + let alices_spaces = rig.spaced.client.wallet_list_spaces(ALICE, None).await?; let alices_balance = rig.spaced.client.wallet_get_balance(ALICE).await?; let result = wallet_do( @@ -101,15 +101,15 @@ async fn it_should_allow_outbidding(rig: &TestRig) -> anyhow::Result<()> { println!("{}", serde_json::to_string_pretty(&result).unwrap()); - let bob_spaces_updated = rig.spaced.client.wallet_list_spaces(BOB).await?; + let bob_spaces_updated = rig.spaced.client.wallet_list_spaces(BOB, None).await?; assert!(bob_spaces_updated.pending.first().is_some_and(|s| s.to_string() == TEST_SPACE), "must be a pending space"); rig.mine_blocks(1, None).await?; rig.wait_until_synced().await?; rig.wait_until_wallet_synced(BOB).await?; rig.wait_until_wallet_synced(ALICE).await?; - let bob_spaces_updated = rig.spaced.client.wallet_list_spaces(BOB).await?; - let alice_spaces_updated = rig.spaced.client.wallet_list_spaces(ALICE).await?; + let bob_spaces_updated = rig.spaced.client.wallet_list_spaces(BOB, None).await?; + let alice_spaces_updated = rig.spaced.client.wallet_list_spaces(ALICE, None).await?; let alices_balance_updated = rig.spaced.client.wallet_get_balance(ALICE).await?; assert_eq!( @@ -189,8 +189,8 @@ async fn it_should_only_accept_forced_zero_value_bid_increments_and_revoke( // Bob outbids alice rig.wait_until_wallet_synced(BOB).await?; rig.wait_until_wallet_synced(EVE).await?; - let eve_spaces = rig.spaced.client.wallet_list_spaces(EVE).await?; - let bob_spaces = rig.spaced.client.wallet_list_spaces(BOB).await?; + let eve_spaces = rig.spaced.client.wallet_list_spaces(EVE, None).await?; + let bob_spaces = rig.spaced.client.wallet_list_spaces(BOB, None).await?; let bob_balance = rig.spaced.client.wallet_get_balance(BOB).await?; let fullspaceout = rig @@ -271,9 +271,9 @@ async fn it_should_only_accept_forced_zero_value_bid_increments_and_revoke( rig.wait_until_synced().await?; rig.wait_until_wallet_synced(BOB).await?; rig.wait_until_wallet_synced(ALICE).await?; - let bob_spaces_updated = rig.spaced.client.wallet_list_spaces(BOB).await?; + let bob_spaces_updated = rig.spaced.client.wallet_list_spaces(BOB, None).await?; let bob_balance_updated = rig.spaced.client.wallet_get_balance(BOB).await?; - let eve_spaces_updated = rig.spaced.client.wallet_list_spaces(EVE).await?; + let eve_spaces_updated = rig.spaced.client.wallet_list_spaces(EVE, None).await?; assert_eq!( bob_spaces.winning.len() - 1, @@ -320,7 +320,7 @@ async fn it_should_allow_claim_on_or_after_claim_height(rig: &TestRig) -> anyhow rig.wait_until_synced().await?; rig.wait_until_wallet_synced(wallet).await?; - let all_spaces = rig.spaced.client.wallet_list_spaces(wallet).await?; + let all_spaces = rig.spaced.client.wallet_list_spaces(wallet, None).await?; let result = wallet_do( rig, @@ -339,7 +339,7 @@ async fn it_should_allow_claim_on_or_after_claim_height(rig: &TestRig) -> anyhow rig.wait_until_synced().await?; rig.wait_until_wallet_synced(wallet).await?; - let all_spaces_2 = rig.spaced.client.wallet_list_spaces(wallet).await?; + let all_spaces_2 = rig.spaced.client.wallet_list_spaces(wallet, None).await?; assert_eq!( all_spaces.owned.len() + 1, @@ -367,7 +367,7 @@ async fn it_should_allow_batch_transfers_refreshing_expire_height( ) -> anyhow::Result<()> { rig.wait_until_wallet_synced(ALICE).await?; rig.wait_until_synced().await?; - let all_spaces = rig.spaced.client.wallet_list_spaces(ALICE).await?; + let all_spaces = rig.spaced.client.wallet_list_spaces(ALICE, None).await?; let registered_spaces: Vec<_> = all_spaces .owned .iter() @@ -399,7 +399,7 @@ async fn it_should_allow_batch_transfers_refreshing_expire_height( rig.wait_until_synced().await?; rig.wait_until_wallet_synced(ALICE).await?; - let all_spaces_2 = rig.spaced.client.wallet_list_spaces(ALICE).await?; + let all_spaces_2 = rig.spaced.client.wallet_list_spaces(ALICE, None).await?; assert_eq!( all_spaces.owned.len(), @@ -432,7 +432,7 @@ async fn it_should_allow_batch_transfers_refreshing_expire_height( async fn it_should_allow_applying_script_in_batch(rig: &TestRig) -> anyhow::Result<()> { rig.wait_until_wallet_synced(ALICE).await?; rig.wait_until_synced().await?; - let all_spaces = rig.spaced.client.wallet_list_spaces(ALICE).await?; + let all_spaces = rig.spaced.client.wallet_list_spaces(ALICE, None).await?; let registered_spaces: Vec<_> = all_spaces .owned .iter() @@ -465,7 +465,7 @@ async fn it_should_allow_applying_script_in_batch(rig: &TestRig) -> anyhow::Resu rig.wait_until_synced().await?; rig.wait_until_wallet_synced(ALICE).await?; - let all_spaces_2 = rig.spaced.client.wallet_list_spaces(ALICE).await?; + let all_spaces_2 = rig.spaced.client.wallet_list_spaces(ALICE, None).await?; assert_eq!( all_spaces.owned.len(), @@ -998,7 +998,7 @@ async fn it_can_batch_txs(rig: &TestRig) -> anyhow::Result<()> { let bob_spaces = rig .spaced .client - .wallet_list_spaces(BOB) + .wallet_list_spaces(BOB, None) .await .expect("bob spaces"); assert!( @@ -1017,7 +1017,7 @@ async fn it_can_batch_txs(rig: &TestRig) -> anyhow::Result<()> { let alice_spaces = rig .spaced .client - .wallet_list_spaces(ALICE) + .wallet_list_spaces(ALICE, None) .await .expect("alice spaces"); let batch1 = alice_spaces @@ -1166,7 +1166,7 @@ async fn it_should_allow_buy_sell(rig: &TestRig) -> anyhow::Result<()> { let alice_spaces = rig .spaced .client - .wallet_list_spaces(ALICE) + .wallet_list_spaces(ALICE, None) .await .expect("alice spaces"); let space = alice_spaces @@ -1227,7 +1227,7 @@ async fn it_should_allow_buy_sell(rig: &TestRig) -> anyhow::Result<()> { let bob_spaces = rig .spaced .client - .wallet_list_spaces(BOB) + .wallet_list_spaces(BOB, None) .await .expect("bob spaces"); @@ -1260,7 +1260,7 @@ async fn it_should_allow_sign_verify_messages(rig: &TestRig) -> anyhow::Result<( let alice_spaces = rig .spaced .client - .wallet_list_spaces(BOB) + .wallet_list_spaces(BOB, None) .await .expect("bob spaces"); let space = alice_spaces