Skip to content

Commit 847266a

Browse files
committed
feat: add seen at to activity
1 parent 0e66950 commit 847266a

File tree

4 files changed

+180
-23
lines changed

4 files changed

+180
-23
lines changed

src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,15 @@ pub fn is_address_used(address: String) -> Result<bool, ActivityError> {
14351435
db.is_address_used(&address)
14361436
}
14371437

1438+
#[uniffi::export]
1439+
pub fn mark_activity_as_seen(activity_id: String, seen_at: u64) -> Result<(), ActivityError> {
1440+
let mut guard = get_activity_db()?;
1441+
let db = guard.activity_db.as_mut().ok_or(ActivityError::ConnectionError {
1442+
error_details: "Database not initialized. Call init_db first.".to_string()
1443+
})?;
1444+
db.mark_activity_as_seen(&activity_id, seen_at)
1445+
}
1446+
14381447
#[uniffi::export]
14391448
pub async fn blocktank_remove_all_orders() -> Result<(), BlocktankError> {
14401449
let rt = ensure_runtime();

src/modules/activity/implementation.rs

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const CREATE_ACTIVITIES_TABLE: &str = "
1212
tx_type TEXT NOT NULL CHECK (tx_type IN ('sent', 'received')),
1313
timestamp INTEGER NOT NULL CHECK (timestamp > 0),
1414
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
15-
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
15+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
16+
seen_at INTEGER CHECK (seen_at IS NULL OR seen_at > 0)
1617
)";
1718

1819
const CREATE_ONCHAIN_TABLE: &str = "
@@ -641,6 +642,7 @@ impl ActivityDB {
641642
a.timestamp,
642643
a.created_at,
643644
a.updated_at,
645+
a.seen_at,
644646
645647
-- Onchain columns
646648
o.tx_id AS onchain_tx_id,
@@ -688,11 +690,12 @@ impl ActivityDB {
688690
let timestamp: i64 = row.get(3)?;
689691
let created_at: Option<i64> = row.get(4)?;
690692
let updated_at: Option<i64> = row.get(5)?;
691-
let value: i64 = row.get(7)?;
692-
let fee: i64 = row.get(8)?;
693-
let fee_rate: i64 = row.get(9)?;
694-
let confirm_timestamp: Option<i64> = row.get(16)?;
695-
let boost_tx_ids_str: String = row.get(13)?;
693+
let seen_at: Option<i64> = row.get(6)?;
694+
let value: i64 = row.get(8)?;
695+
let fee: i64 = row.get(9)?;
696+
let fee_rate: i64 = row.get(10)?;
697+
let confirm_timestamp: Option<i64> = row.get(17)?;
698+
let boost_tx_ids_str: String = row.get(14)?;
696699
let boost_tx_ids: Vec<String> = if boost_tx_ids_str.is_empty() {
697700
Vec::new()
698701
} else {
@@ -705,40 +708,43 @@ impl ActivityDB {
705708
timestamp: timestamp as u64,
706709
created_at: created_at.map(|t| t as u64),
707710
updated_at: updated_at.map(|t| t as u64),
708-
tx_id: row.get(6)?,
711+
seen_at: seen_at.map(|t| t as u64),
712+
tx_id: row.get(7)?,
709713
value: value as u64,
710714
fee: fee as u64,
711715
fee_rate: fee_rate as u64,
712-
address: row.get(10)?,
713-
confirmed: row.get(11)?,
714-
is_boosted: row.get(12)?,
716+
address: row.get(11)?,
717+
confirmed: row.get(12)?,
718+
is_boosted: row.get(13)?,
715719
boost_tx_ids,
716-
is_transfer: row.get(14)?,
717-
does_exist: row.get(15)?,
720+
is_transfer: row.get(15)?,
721+
does_exist: row.get(16)?,
718722
confirm_timestamp: confirm_timestamp.map(|t| t as u64),
719-
channel_id: row.get(17)?,
720-
transfer_tx_id: row.get(18)?,
723+
channel_id: row.get(18)?,
724+
transfer_tx_id: row.get(19)?,
721725
}))
722726
}
723727
"lightning" => {
724728
let timestamp: i64 = row.get(3)?;
725729
let created_at: Option<i64> = row.get(4)?;
726730
let updated_at: Option<i64> = row.get(5)?;
727-
let value: i64 = row.get(20)?;
728-
let fee: Option<i64> = row.get(22)?;
731+
let seen_at: Option<i64> = row.get(6)?;
732+
let value: i64 = row.get(21)?;
733+
let fee: Option<i64> = row.get(23)?;
729734

730735
Ok(Activity::Lightning(LightningActivity {
731736
id: row.get(0)?,
732737
tx_type: Self::parse_payment_type(row, 2)?,
733738
timestamp: timestamp as u64,
734739
created_at: created_at.map(|t| t as u64),
735740
updated_at: updated_at.map(|t| t as u64),
736-
invoice: row.get(19)?,
741+
seen_at: seen_at.map(|t| t as u64),
742+
invoice: row.get(20)?,
737743
value: value as u64,
738-
status: Self::parse_payment_state(row, 21)?,
744+
status: Self::parse_payment_state(row, 22)?,
739745
fee: fee.map(|f| f as u64),
740-
message: row.get(23)?,
741-
preimage: row.get(24)?,
746+
message: row.get(24)?,
747+
preimage: row.get(25)?,
742748
}))
743749
}
744750
_ => Err(rusqlite::Error::InvalidColumnType(
@@ -783,7 +789,7 @@ impl ActivityDB {
783789
a.id, a.tx_type, o.tx_id, o.value, o.fee, o.fee_rate,
784790
o.address, o.confirmed, a.timestamp, o.is_boosted,
785791
o.boost_tx_ids, o.is_transfer, o.does_exist, o.confirm_timestamp,
786-
o.channel_id, o.transfer_tx_id, a.created_at, a.updated_at
792+
o.channel_id, o.transfer_tx_id, a.created_at, a.updated_at, a.seen_at
787793
FROM activities a
788794
JOIN onchain_activity o ON a.id = o.id
789795
WHERE a.id = ?1";
@@ -800,6 +806,7 @@ impl ActivityDB {
800806
let confirm_timestamp: Option<i64> = row.get(13)?;
801807
let created_at: Option<i64> = row.get(16)?;
802808
let updated_at: Option<i64> = row.get(17)?;
809+
let seen_at: Option<i64> = row.get(18)?;
803810
let boost_tx_ids_str: String = row.get(10)?;
804811
let boost_tx_ids: Vec<String> = if boost_tx_ids_str.is_empty() {
805812
Vec::new()
@@ -826,6 +833,7 @@ impl ActivityDB {
826833
transfer_tx_id: row.get(15)?,
827834
created_at: created_at.map(|t| t as u64),
828835
updated_at: updated_at.map(|t| t as u64),
836+
seen_at: seen_at.map(|t| t as u64),
829837
}))
830838
}) {
831839
Ok(activity) => Ok(Some(activity)),
@@ -841,7 +849,7 @@ impl ActivityDB {
841849
SELECT
842850
a.id, a.tx_type, l.status, l.value, l.fee,
843851
l.invoice, l.message, a.timestamp,
844-
l.preimage, a.created_at, a.updated_at
852+
l.preimage, a.created_at, a.updated_at, a.seen_at
845853
FROM activities a
846854
JOIN lightning_activity l ON a.id = l.id
847855
WHERE a.id = ?1";
@@ -856,6 +864,7 @@ impl ActivityDB {
856864
let timestamp: i64 = row.get(7)?;
857865
let created_at: Option<i64> = row.get(9)?;
858866
let updated_at: Option<i64> = row.get(10)?;
867+
let seen_at: Option<i64> = row.get(11)?;
859868

860869
Ok(Activity::Lightning(LightningActivity {
861870
id: row.get(0)?,
@@ -869,6 +878,7 @@ impl ActivityDB {
869878
preimage: row.get(8)?,
870879
created_at: created_at.map(|t| t as u64),
871880
updated_at: updated_at.map(|t| t as u64),
881+
seen_at: seen_at.map(|t| t as u64),
872882
}))
873883
}).map_err(|e| ActivityError::RetrievalError {
874884
error_details: format!("Failed to get lightning activity: {}", e),
@@ -887,7 +897,7 @@ impl ActivityDB {
887897
a.id, a.tx_type, o.tx_id, o.value, o.fee, o.fee_rate,
888898
o.address, o.confirmed, a.timestamp, o.is_boosted,
889899
o.boost_tx_ids, o.is_transfer, o.does_exist, o.confirm_timestamp,
890-
o.channel_id, o.transfer_tx_id, a.created_at, a.updated_at
900+
o.channel_id, o.transfer_tx_id, a.created_at, a.updated_at, a.seen_at
891901
FROM activities a
892902
JOIN onchain_activity o ON a.id = o.id
893903
WHERE o.tx_id = ?1 AND a.activity_type = 'onchain'
@@ -905,6 +915,7 @@ impl ActivityDB {
905915
let confirm_timestamp: Option<i64> = row.get(13)?;
906916
let created_at: Option<i64> = row.get(16)?;
907917
let updated_at: Option<i64> = row.get(17)?;
918+
let seen_at: Option<i64> = row.get(18)?;
908919
let boost_tx_ids_str: String = row.get(10)?;
909920
let boost_tx_ids: Vec<String> = if boost_tx_ids_str.is_empty() {
910921
Vec::new()
@@ -931,6 +942,7 @@ impl ActivityDB {
931942
transfer_tx_id: row.get(15)?,
932943
created_at: created_at.map(|t| t as u64),
933944
updated_at: updated_at.map(|t| t as u64),
945+
seen_at: seen_at.map(|t| t as u64),
934946
})
935947
}) {
936948
Ok(activity) => Ok(Some(activity)),
@@ -1081,6 +1093,24 @@ impl ActivityDB {
10811093
Ok(())
10821094
}
10831095

1096+
/// Marks an activity as seen by setting the seen_at timestamp.
1097+
pub fn mark_activity_as_seen(&mut self, activity_id: &str, seen_at: u64) -> Result<(), ActivityError> {
1098+
let rows = self.conn.execute(
1099+
"UPDATE activities SET seen_at = ?1 WHERE id = ?2",
1100+
rusqlite::params![seen_at as i64, activity_id],
1101+
).map_err(|e| ActivityError::DataError {
1102+
error_details: format!("Failed to mark activity as seen: {}", e),
1103+
})?;
1104+
1105+
if rows == 0 {
1106+
return Err(ActivityError::DataError {
1107+
error_details: "No activity found with given ID".to_string(),
1108+
});
1109+
}
1110+
1111+
Ok(())
1112+
}
1113+
10841114
/// Deletes an activity and associated data.
10851115
pub fn delete_activity_by_id(&mut self, activity_id: &str) -> Result<bool, ActivityError> {
10861116
let tx = self.conn.transaction().map_err(|e| ActivityError::DataError {

src/modules/activity/tests.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ mod tests {
3434
transfer_tx_id: None,
3535
created_at: None,
3636
updated_at: None,
37+
seen_at: None,
3738
}
3839
}
3940

@@ -50,6 +51,7 @@ mod tests {
5051
preimage: Some("preimage123".to_string()),
5152
created_at: None,
5253
updated_at: None,
54+
seen_at: None,
5355
}
5456
}
5557

@@ -3536,4 +3538,109 @@ mod tests {
35363538

35373539
cleanup(&db_path);
35383540
}
3541+
3542+
#[test]
3543+
fn test_mark_activity_as_seen_onchain() {
3544+
let (mut db, db_path) = setup();
3545+
let activity = create_test_onchain_activity();
3546+
db.insert_onchain_activity(&activity).unwrap();
3547+
3548+
// Verify initial state - seen_at should be None
3549+
let retrieved = db.get_activity_by_id(&activity.id).unwrap().unwrap();
3550+
assert!(retrieved.get_seen_at().is_none(), "seen_at should be None initially");
3551+
3552+
// Mark as seen
3553+
let seen_timestamp = 1234567900u64;
3554+
db.mark_activity_as_seen(&activity.id, seen_timestamp).unwrap();
3555+
3556+
// Verify seen_at is now set
3557+
let retrieved = db.get_activity_by_id(&activity.id).unwrap().unwrap();
3558+
assert_eq!(retrieved.get_seen_at(), Some(seen_timestamp), "seen_at should be set");
3559+
3560+
cleanup(&db_path);
3561+
}
3562+
3563+
#[test]
3564+
fn test_mark_activity_as_seen_lightning() {
3565+
let (mut db, db_path) = setup();
3566+
let activity = create_test_lightning_activity();
3567+
db.insert_lightning_activity(&activity).unwrap();
3568+
3569+
// Verify initial state - seen_at should be None
3570+
let retrieved = db.get_activity_by_id(&activity.id).unwrap().unwrap();
3571+
assert!(retrieved.get_seen_at().is_none(), "seen_at should be None initially");
3572+
3573+
// Mark as seen
3574+
let seen_timestamp = 1234567900u64;
3575+
db.mark_activity_as_seen(&activity.id, seen_timestamp).unwrap();
3576+
3577+
// Verify seen_at is now set
3578+
let retrieved = db.get_activity_by_id(&activity.id).unwrap().unwrap();
3579+
assert_eq!(retrieved.get_seen_at(), Some(seen_timestamp), "seen_at should be set");
3580+
3581+
cleanup(&db_path);
3582+
}
3583+
3584+
#[test]
3585+
fn test_mark_activity_as_seen_nonexistent() {
3586+
let (mut db, db_path) = setup();
3587+
3588+
// Try to mark a non-existent activity as seen
3589+
let result = db.mark_activity_as_seen("nonexistent_id", 1234567900);
3590+
assert!(result.is_err(), "Should fail for non-existent activity");
3591+
3592+
cleanup(&db_path);
3593+
}
3594+
3595+
#[test]
3596+
fn test_seen_at_preserved_in_get_activities() {
3597+
let (mut db, db_path) = setup();
3598+
3599+
// Insert two activities
3600+
let mut onchain = create_test_onchain_activity();
3601+
onchain.timestamp = 1000;
3602+
let mut lightning = create_test_lightning_activity();
3603+
lightning.timestamp = 2000;
3604+
3605+
db.insert_onchain_activity(&onchain).unwrap();
3606+
db.insert_lightning_activity(&lightning).unwrap();
3607+
3608+
// Mark only onchain as seen
3609+
let seen_timestamp = 3000u64;
3610+
db.mark_activity_as_seen(&onchain.id, seen_timestamp).unwrap();
3611+
3612+
// Get all activities
3613+
let activities = db.get_activities(None, None, None, None, None, None, None, None).unwrap();
3614+
assert_eq!(activities.len(), 2);
3615+
3616+
for activity in activities {
3617+
match activity {
3618+
Activity::Onchain(o) => {
3619+
assert_eq!(o.seen_at, Some(seen_timestamp), "Onchain should have seen_at set");
3620+
}
3621+
Activity::Lightning(l) => {
3622+
assert!(l.seen_at.is_none(), "Lightning should not have seen_at set");
3623+
}
3624+
}
3625+
}
3626+
3627+
cleanup(&db_path);
3628+
}
3629+
3630+
#[test]
3631+
fn test_seen_at_preserved_in_get_activity_by_tx_id() {
3632+
let (mut db, db_path) = setup();
3633+
let activity = create_test_onchain_activity();
3634+
db.insert_onchain_activity(&activity).unwrap();
3635+
3636+
// Mark as seen
3637+
let seen_timestamp = 1234567900u64;
3638+
db.mark_activity_as_seen(&activity.id, seen_timestamp).unwrap();
3639+
3640+
// Retrieve by tx_id and verify seen_at
3641+
let retrieved = db.get_activity_by_tx_id(&activity.tx_id).unwrap().unwrap();
3642+
assert_eq!(retrieved.seen_at, Some(seen_timestamp), "seen_at should be preserved when getting by tx_id");
3643+
3644+
cleanup(&db_path);
3645+
}
35393646
}

src/modules/activity/types.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ impl Activity {
5151
Activity::Lightning(l) => l.updated_at,
5252
}
5353
}
54+
55+
pub fn get_seen_at(&self) -> Option<u64> {
56+
match self {
57+
Activity::Onchain(o) => o.seen_at,
58+
Activity::Lightning(l) => l.seen_at,
59+
}
60+
}
5461
}
5562

5663
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, uniffi::Enum)]
@@ -96,6 +103,8 @@ pub struct OnchainActivity {
96103
pub created_at: Option<u64>,
97104
#[serde(skip_serializing_if = "Option::is_none")]
98105
pub updated_at: Option<u64>,
106+
#[serde(skip_serializing_if = "Option::is_none")]
107+
pub seen_at: Option<u64>,
99108
}
100109

101110
#[derive(Debug, Serialize, Deserialize, Clone, uniffi::Record)]
@@ -113,6 +122,8 @@ pub struct LightningActivity {
113122
pub created_at: Option<u64>,
114123
#[serde(skip_serializing_if = "Option::is_none")]
115124
pub updated_at: Option<u64>,
125+
#[serde(skip_serializing_if = "Option::is_none")]
126+
pub seen_at: Option<u64>,
116127
}
117128

118129
#[derive(Debug, Serialize, Deserialize, Clone, uniffi::Record)]

0 commit comments

Comments
 (0)