Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Add `get_activities_tags(wallet_id: Option<String>)` and `get_pre_activity_metadata_list(wallet_id: Option<String>)`, wallet-scoped reads of the two tag-backup tables where `None` returns every scope. Apps backing up a single wallet scope (for example a `trezor:{hash}` hardware wallet) can now name that scope in the call instead of fetching every scope and filtering client-side, so which records leave the device is visible in the FFI call. The unscoped `get_all_activities_tags()` and `get_all_pre_activity_metadata()` are unchanged and now delegate to the scoped versions.

- The Android AAR now ships targeted R8 consumer keep rules for the UniFFI/JNA FFI surface, so consuming apps can enable R8 full mode without extra keep rules for this library.
- Add a generic hardware-wallet catalog with Foundation Passport support, multipart UR QR encoding and decoding, Passport single-signature account export parsing, and signed PSBT finalization across the UniFFI bindings.
- Swap status updates now reconcile against Boltz's REST status whenever a swap is (re)subscribed, both on `boltz_start_swap_updates` and when `boltz_create_reverse_swap` adds a swap to a running stream. A confirmed reverse-swap lockup is therefore caught up and auto-claimed even when its live WebSocket event was missed (for example because the updates stream was down while the lockup confirmed), instead of the swap silently stalling until a manual claim. No FFI signature change.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@
```rust
fn get_all_unique_tags() -> Result<Vec<String>, ActivityError>
```
- [get_activities_tags](src/modules/activity/README.md#usage-examples): Get activity tags for one wallet scope, or every scope when `wallet_id` is `None`
```rust
fn get_activities_tags(wallet_id: Option<String>) -> Result<Vec<ActivityTags>, ActivityError>
```
- [get_activities_by_tag](src/modules/activity/README.md#usage-examples): Get activities with a specific tag and optional wallet scope
```rust
fn get_activities_by_tag(
Expand All @@ -205,6 +209,10 @@
search_by_address: bool
) -> Result<Option<PreActivityMetadata>, ActivityError>
```
- [get_pre_activity_metadata_list](src/modules/activity/README.md#usage-examples): Get pending metadata for one wallet scope, or every scope when `wallet_id` is `None`
```rust
fn get_pre_activity_metadata_list(wallet_id: Option<String>) -> Result<Vec<PreActivityMetadata>, ActivityError>
```
- [add_pre_activity_metadata_tags](src/modules/activity/README.md#usage-examples): Add tags to pending metadata
```rust
fn add_pre_activity_metadata_tags(wallet_id: String, payment_id: String, tags: Vec<String>) -> Result<(), ActivityError>
Expand Down
166 changes: 166 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,19 @@ pub fn get_all_activities_tags() -> Result<Vec<ActivityTags>, ActivityError> {
db.get_all_activities_tags()
}

/// Activity tags for a single wallet scope, or every scope when `wallet_id` is `None`.
#[uniffi::export]
pub fn get_activities_tags(wallet_id: Option<String>) -> Result<Vec<ActivityTags>, ActivityError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see that the new database methods are covered, but no test invokes the exported get_activities_tags or get_pre_activity_metadata_list functions in src/lib.rs; the added tests call ActivityDB directly. If either wrapper dropped wallet_id or delegated to its unscoped getter, every added test would still pass while native clients could receive records from every wallet scope. Could we add one exported-entry-point test that initializes two wallet scopes and asserts both functions preserve Some("trezor:...") and None semantics?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, added in 6a9769d. test_exported_scoped_getters_preserve_wallet_id in src/lib.rs runs init_db, seeds two wallet scopes through the exported entry points, and asserts both get_activities_tags and get_pre_activity_metadata_list preserve Some("trezor:...") and None semantics, plus that an unknown scope returns empty.

let guard = get_activity_db()?;
let db = guard
.activity_db
.as_ref()
.ok_or(ActivityError::ConnectionError {
error_details: "Database not initialized. Call init_db first.".to_string(),
})?;
db.get_activities_tags(wallet_id.as_deref())
}

#[uniffi::export]
pub fn upsert_tags(activity_tags: Vec<ActivityTags>) -> Result<(), ActivityError> {
let mut guard = get_activity_db()?;
Expand Down Expand Up @@ -882,6 +895,21 @@ pub fn get_all_pre_activity_metadata() -> Result<Vec<PreActivityMetadata>, Activ
db.get_all_pre_activity_metadata()
}

/// Pre-activity metadata for a single wallet scope, or every scope when `wallet_id` is `None`.
#[uniffi::export]
pub fn get_pre_activity_metadata_list(
wallet_id: Option<String>,
) -> Result<Vec<PreActivityMetadata>, ActivityError> {
let guard = get_activity_db()?;
let db = guard
.activity_db
.as_ref()
.ok_or(ActivityError::ConnectionError {
error_details: "Database not initialized. Call init_db first.".to_string(),
})?;
db.get_pre_activity_metadata_list(wallet_id.as_deref())
}

#[uniffi::export]
pub fn upsert_closed_channel(channel: ClosedChannelDetails) -> Result<(), ActivityError> {
let mut guard = get_activity_db()?;
Expand Down Expand Up @@ -3011,3 +3039,141 @@ pub async fn boltz_stop_swap_updates() {
.spawn(async move { boltz::stop_swap_updates().await })
.await;
}

#[cfg(test)]
mod tests {
use super::*;

/// Drives the exported entry points through the global database, so a
/// wrapper that dropped `wallet_id` or fell back to an unscoped getter
/// would fail here even while the `ActivityDB` tests pass.
#[test]
fn test_exported_scoped_getters_preserve_wallet_id() {
let base_path =
std::env::temp_dir().join(format!("bitkitcore_lib_test_{}", rand::random::<u64>()));
std::fs::create_dir_all(&base_path).unwrap();
init_db(base_path.to_string_lossy().into_owned()).unwrap();

let hardware_wallet_id = format!("trezor:{}", "c".repeat(64));

let mut activity = OnchainActivity {
wallet_id: DEFAULT_WALLET_ID.to_string(),
id: "main_activity".to_string(),
tx_type: PaymentType::Sent,
tx_id: "main_txid".to_string(),
value: 50000,
fee: 500,
fee_rate: 1,
address: "bc1q...".to_string(),
confirmed: true,
timestamp: 1234567890,
is_boosted: false,
boost_tx_ids: vec![],
is_transfer: false,
does_exist: true,
confirm_timestamp: Some(1234568890),
channel_id: None,
transfer_tx_id: None,
contact: None,
created_at: None,
updated_at: None,
seen_at: None,
};
insert_activity(Activity::Onchain(activity.clone())).unwrap();
activity.wallet_id = hardware_wallet_id.clone();
activity.id = "hardware_activity".to_string();
activity.tx_id = "hardware_txid".to_string();
insert_activity(Activity::Onchain(activity)).unwrap();

upsert_tags(vec![
ActivityTags {
wallet_id: DEFAULT_WALLET_ID.to_string(),
activity_id: "main_activity".to_string(),
tags: vec!["main".to_string()],
},
ActivityTags {
wallet_id: hardware_wallet_id.clone(),
activity_id: "hardware_activity".to_string(),
tags: vec!["hardware".to_string()],
},
])
.unwrap();

let default_tags = get_activities_tags(Some(DEFAULT_WALLET_ID.to_string())).unwrap();
assert_eq!(default_tags.len(), 1);
assert_eq!(default_tags[0].wallet_id, DEFAULT_WALLET_ID);
assert_eq!(default_tags[0].tags, vec!["main".to_string()]);

let hardware_tags = get_activities_tags(Some(hardware_wallet_id.clone())).unwrap();
assert_eq!(hardware_tags.len(), 1);
assert_eq!(hardware_tags[0].wallet_id, hardware_wallet_id);
assert_eq!(hardware_tags[0].tags, vec!["hardware".to_string()]);

let tag_scopes = |tags: &[ActivityTags]| {
tags.iter()
.map(|entry| (entry.wallet_id.clone(), entry.activity_id.clone()))
.collect::<Vec<_>>()
};
let all_tags = get_activities_tags(None).unwrap();
assert_eq!(all_tags.len(), 2);
assert_eq!(
tag_scopes(&all_tags),
tag_scopes(&get_all_activities_tags().unwrap())
);

let mut hardware_metadata = PreActivityMetadata {
wallet_id: DEFAULT_WALLET_ID.to_string(),
payment_id: "bc1qmain".to_string(),
tags: vec!["main".to_string()],
payment_hash: None,
tx_id: None,
address: None,
is_receive: false,
fee_rate: 0,
is_transfer: false,
channel_id: None,
created_at: 0,
};
add_pre_activity_metadata(hardware_metadata.clone()).unwrap();
hardware_metadata.wallet_id = hardware_wallet_id.clone();
hardware_metadata.payment_id = "bc1qhardware".to_string();
hardware_metadata.tags = vec!["hardware".to_string()];
add_pre_activity_metadata(hardware_metadata).unwrap();

let default_metadata =
get_pre_activity_metadata_list(Some(DEFAULT_WALLET_ID.to_string())).unwrap();
assert_eq!(default_metadata.len(), 1);
assert_eq!(default_metadata[0].wallet_id, DEFAULT_WALLET_ID);
assert_eq!(default_metadata[0].payment_id, "bc1qmain");

let hardware_metadata =
get_pre_activity_metadata_list(Some(hardware_wallet_id.clone())).unwrap();
assert_eq!(hardware_metadata.len(), 1);
assert_eq!(hardware_metadata[0].wallet_id, hardware_wallet_id);
assert_eq!(hardware_metadata[0].payment_id, "bc1qhardware");

let metadata_scopes = |metadata: &[PreActivityMetadata]| {
metadata
.iter()
.map(|entry| (entry.wallet_id.clone(), entry.payment_id.clone()))
.collect::<Vec<_>>()
};
let all_metadata = get_pre_activity_metadata_list(None).unwrap();
assert_eq!(all_metadata.len(), 2);
assert_eq!(
metadata_scopes(&all_metadata),
metadata_scopes(&get_all_pre_activity_metadata().unwrap())
);

assert!(get_activities_tags(Some("ledger:unknown".to_string()))
.unwrap()
.is_empty());
assert!(
get_pre_activity_metadata_list(Some("ledger:unknown".to_string()))
.unwrap()
.is_empty()
);

std::fs::remove_dir_all(&base_path).ok();
}
}
5 changes: 5 additions & 0 deletions src/modules/activity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ fn add_tags(wallet_id: String, activity_id: String, tags: Vec<String>) -> Result
fn remove_tags(wallet_id: String, activity_id: String, tags: Vec<String>) -> Result<(), ActivityError>
fn get_tags(wallet_id: String, activity_id: String) -> Result<Vec<String>, ActivityError>
fn get_all_unique_tags() -> Result<Vec<String>, ActivityError>
fn get_all_activities_tags() -> Result<Vec<ActivityTags>, ActivityError>
// Wallet-scoped tag read; `None` returns every wallet scope
fn get_activities_tags(wallet_id: Option<String>) -> Result<Vec<ActivityTags>, ActivityError>

// Pre-activity metadata
fn add_pre_activity_metadata(pre_activity_metadata: PreActivityMetadata) -> Result<(), ActivityError>
Expand All @@ -85,6 +88,8 @@ fn get_pre_activity_metadata(
search_by_address: bool
) -> Result<Option<PreActivityMetadata>, ActivityError>
fn get_all_pre_activity_metadata() -> Result<Vec<PreActivityMetadata>, ActivityError>
// Wallet-scoped metadata read; `None` returns every wallet scope
fn get_pre_activity_metadata_list(wallet_id: Option<String>) -> Result<Vec<PreActivityMetadata>, ActivityError>

// Transaction details
fn upsert_transaction_details(details_list: Vec<TransactionDetails>) -> Result<(), ActivityError>
Expand Down
73 changes: 58 additions & 15 deletions src/modules/activity/implementation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2149,21 +2149,42 @@ impl ActivityDB {
Ok(tags)
}

/// Get all activity tags for backup
/// Get all activity tags for backup, across every wallet scope
pub fn get_all_activities_tags(&self) -> Result<Vec<ActivityTags>, ActivityError> {
self.get_activities_tags(None)
}

/// Get activity tags for backup, scoped to `wallet_id` when it is `Some`
/// and covering every wallet scope when it is `None`.
pub fn get_activities_tags(
&self,
wallet_id: Option<&str>,
) -> Result<Vec<ActivityTags>, ActivityError> {
let wallet_id = wallet_id.map(Self::normalize_wallet_id).transpose()?;
let wallet_filter = if wallet_id.is_some() {
" WHERE wallet_id = ?1"
} else {
""
};
let sql = format!(
"SELECT wallet_id, activity_id, tag
FROM activity_tags{}
ORDER BY wallet_id, activity_id, tag",
wallet_filter
);

let mut stmt = self
.conn
.prepare(
"SELECT wallet_id, activity_id, tag
FROM activity_tags
ORDER BY wallet_id, activity_id, tag",
)
.prepare(&sql)
.map_err(|e| ActivityError::RetrievalError {
error_details: format!("Failed to prepare statement: {}", e),
})?;

let params: Vec<&String> = wallet_id.iter().collect();
let rows: Vec<(String, String, String)> = stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
.query_map(rusqlite::params_from_iter(params), |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
})
.map_err(|e| ActivityError::RetrievalError {
error_details: format!("Failed to execute query: {}", e),
})?
Expand Down Expand Up @@ -2653,15 +2674,37 @@ impl ActivityDB {
}
}

/// Get all pre-activity metadata for backup
#[allow(clippy::type_complexity)]
/// Get all pre-activity metadata for backup, across every wallet scope
pub fn get_all_pre_activity_metadata(&self) -> Result<Vec<PreActivityMetadata>, ActivityError> {
let mut stmt = self.conn.prepare(
"SELECT wallet_id, payment_id, tags, payment_hash, tx_id, address, is_receive, fee_rate, is_transfer, channel_id, created_at FROM pre_activity_metadata ORDER BY wallet_id, payment_id"
).map_err(|e| ActivityError::RetrievalError {
error_details: format!("Failed to prepare statement: {}", e),
})?;
self.get_pre_activity_metadata_list(None)
}

/// Get pre-activity metadata for backup, scoped to `wallet_id` when it is
/// `Some` and covering every wallet scope when it is `None`.
#[allow(clippy::type_complexity)]
pub fn get_pre_activity_metadata_list(
&self,
wallet_id: Option<&str>,
) -> Result<Vec<PreActivityMetadata>, ActivityError> {
let wallet_id = wallet_id.map(Self::normalize_wallet_id).transpose()?;
let wallet_filter = if wallet_id.is_some() {
" WHERE wallet_id = ?1"
} else {
""
};
let sql = format!(
"SELECT wallet_id, payment_id, tags, payment_hash, tx_id, address, is_receive, fee_rate, is_transfer, channel_id, created_at FROM pre_activity_metadata{} ORDER BY wallet_id, payment_id",
wallet_filter
);

let mut stmt = self
.conn
.prepare(&sql)
.map_err(|e| ActivityError::RetrievalError {
error_details: format!("Failed to prepare statement: {}", e),
})?;

let params: Vec<&String> = wallet_id.iter().collect();
let rows: Vec<(
String,
String,
Expand All @@ -2675,7 +2718,7 @@ impl ActivityDB {
Option<String>,
i64,
)> = stmt
.query_map([], |row| {
.query_map(rusqlite::params_from_iter(params), |row| {
Ok((
row.get(0)?,
row.get(1)?,
Expand Down
Loading