use std::sync::Arc;
use async_trait::async_trait;
use crate::error::WalletError;
use crate::monitor::task_trait::WalletMonitorTask;
use crate::storage::manager::WalletStorageManager;
use crate::wallet::types::{AuthId, SPEC_OP_INVALID_CHANGE};
pub struct TaskReviewUtxos {
storage: Arc<WalletStorageManager>,
}
impl TaskReviewUtxos {
pub fn new(storage: Arc<WalletStorageManager>) -> Self {
Self { storage }
}
pub async fn review_by_identity_key(
&self,
identity_key: &str,
mode: &str,
) -> Result<String, WalletError> {
let user = self.storage.find_user_by_identity_key(identity_key).await?;
let user = match user {
Some(u) => u,
None => {
return Ok(format!("identityKey {identity_key} was not found\n"));
}
};
let tags = if mode == "all" {
vec!["release".to_string(), "all".to_string()]
} else {
vec!["release".to_string()]
};
let auth = AuthId {
identity_key: user.identity_key.clone(),
user_id: Some(user.user_id),
is_active: None,
};
let args = bsv::wallet::interfaces::ListOutputsArgs {
basket: SPEC_OP_INVALID_CHANGE.to_string(),
tags: tags.clone(),
tag_query_mode: Some(bsv::wallet::interfaces::QueryMode::All),
include: None,
include_custom_instructions: bsv::wallet::types::BooleanDefaultFalse(Some(false)),
include_tags: bsv::wallet::types::BooleanDefaultFalse(Some(false)),
include_labels: bsv::wallet::types::BooleanDefaultFalse(Some(false)),
limit: None,
offset: None,
seek_permission: bsv::wallet::types::BooleanDefaultTrue(Some(false)),
};
let result = self.storage.list_outputs(&auth, &args).await?;
if result.total_outputs == 0 {
return Ok(format!(
"userId {}: no invalid utxos found, {}\n",
user.user_id, user.identity_key
));
}
let total_sats: u64 = result.outputs.iter().map(|o| o.satoshis).sum();
let target = if mode == "all" {
"spendable utxos"
} else {
"spendable change utxos"
};
let has_release = tags.iter().any(|t| t == "release");
let action = if has_release {
"updated to unspendable"
} else {
"found"
};
let mut log = format!(
"userId {}: {} {target} {action}, total {total_sats}, {}\n",
user.user_id, result.total_outputs, user.identity_key
);
for output in &result.outputs {
let spendable = if output.spendable {
"spendable"
} else {
"spent"
};
log += &format!(
" {} {} now {spendable}\n",
output.outpoint, output.satoshis
);
}
Ok(log)
}
}
#[async_trait]
impl WalletMonitorTask for TaskReviewUtxos {
fn storage_manager(&self) -> Option<&WalletStorageManager> {
Some(&self.storage)
}
fn name(&self) -> &str {
"ReviewUtxos"
}
fn trigger(&mut self, _now_msecs: u64) -> bool {
false
}
async fn run_task(&mut self) -> Result<String, WalletError> {
Ok("TaskReviewUtxos is disabled; use review_by_identity_key instead.".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::task_trait::WalletMonitorTask;
use crate::storage::sqlx_impl::SqliteStorage;
use crate::storage::traits::provider::StorageProvider;
use crate::storage::traits::reader_writer::StorageReaderWriter;
use crate::storage::traits::wallet_provider::WalletStorageProvider;
use crate::storage::StorageConfig;
use crate::tables::User;
use crate::types::Chain;
use chrono::Utc;
async fn make_manager_and_storage() -> (Arc<WalletStorageManager>, Arc<SqliteStorage>) {
let cfg = StorageConfig {
url: "sqlite::memory:".to_string(),
..Default::default()
};
let storage = SqliteStorage::new_sqlite(cfg, Chain::Test).await.unwrap();
StorageProvider::migrate_database(&storage).await.unwrap();
let storage = Arc::new(storage);
let provider: Arc<dyn WalletStorageProvider> = storage.clone();
let mgr = Arc::new(WalletStorageManager::new(
"02identity".to_string(),
Some(provider),
vec![],
));
mgr.make_available().await.unwrap();
(mgr, storage)
}
#[tokio::test]
async fn test_trigger_always_returns_false() {
let (mgr, _storage) = make_manager_and_storage().await;
let mut task = TaskReviewUtxos::new(mgr);
assert!(!task.trigger(0));
assert!(!task.trigger(1_000));
assert!(!task.trigger(1_000_000_000));
assert!(!task.trigger(u64::MAX));
}
#[tokio::test]
async fn test_name_matches_registry_identifier() {
let (mgr, _storage) = make_manager_and_storage().await;
let task = TaskReviewUtxos::new(mgr);
assert_eq!(task.name(), "ReviewUtxos");
}
#[tokio::test]
async fn test_run_task_reports_disabled() {
let (mgr, _storage) = make_manager_and_storage().await;
let mut task = TaskReviewUtxos::new(mgr);
let out = task.run_task().await.unwrap();
assert!(
out.contains("disabled") && out.contains("review_by_identity_key"),
"run_task output must mention disabled state and the manual API; got: {out}"
);
}
#[tokio::test]
async fn test_review_by_identity_key_unknown_user() {
let (mgr, _storage) = make_manager_and_storage().await;
let task = TaskReviewUtxos::new(mgr);
let out = task
.review_by_identity_key("03unknown_key", "all")
.await
.unwrap();
assert!(
out.contains("was not found"),
"unknown-user path must report 'was not found'; got: {out}"
);
assert!(
out.contains("03unknown_key"),
"output must echo the identity key; got: {out}"
);
}
#[tokio::test]
async fn test_review_by_identity_key_known_user_no_outputs() {
let (mgr, storage) = make_manager_and_storage().await;
let now = Utc::now().naive_utc();
let user = User {
created_at: now,
updated_at: now,
user_id: 0,
identity_key: "02userkey".to_string(),
active_storage: "default".to_string(),
};
let user_id = storage.insert_user(&user, None).await.unwrap();
assert!(user_id > 0);
let task = TaskReviewUtxos::new(mgr);
let out = task
.review_by_identity_key("02userkey", "all")
.await
.unwrap();
assert!(
out.contains("no invalid utxos found"),
"known-user-no-outputs path must report 'no invalid utxos found'; got: {out}"
);
assert!(
out.contains("02userkey"),
"output must include the identity key; got: {out}"
);
assert!(
out.contains(&format!("userId {user_id}")),
"output must include the user id; got: {out}"
);
}
#[tokio::test]
async fn test_review_by_identity_key_both_modes() {
let (mgr, storage) = make_manager_and_storage().await;
let now = Utc::now().naive_utc();
let user = User {
created_at: now,
updated_at: now,
user_id: 0,
identity_key: "02modeuser".to_string(),
active_storage: "default".to_string(),
};
storage.insert_user(&user, None).await.unwrap();
let task = TaskReviewUtxos::new(mgr);
let out_all = task
.review_by_identity_key("02modeuser", "all")
.await
.unwrap();
let out_change = task
.review_by_identity_key("02modeuser", "change")
.await
.unwrap();
assert!(out_all.contains("no invalid utxos"));
assert!(out_change.contains("no invalid utxos"));
}
}