Skip to content

Commit 0847a1b

Browse files
committed
Add closed channels table
1 parent 423598b commit 0847a1b

File tree

4 files changed

+448
-4
lines changed

4 files changed

+448
-4
lines changed

src/lib.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub use modules::scanner::{
1212
pub use modules::lnurl;
1313
pub use modules::onchain;
1414
pub use modules::activity;
15-
use crate::activity::{ActivityError, ActivityDB, OnchainActivity, LightningActivity, Activity, ActivityFilter, SortDirection, PaymentType, DbError};
15+
use crate::activity::{ActivityError, ActivityDB, OnchainActivity, LightningActivity, Activity, ActivityFilter, SortDirection, PaymentType, DbError, ClosedChannelDetails};
1616
use crate::modules::blocktank::{BlocktankDB, BlocktankError, IBtInfo, IBtOrder, CreateOrderOptions, BtOrderState2, IBt0ConfMinTxFeeWindow, IBtEstimateFeeResponse, IBtEstimateFeeResponse2, CreateCjitOptions, ICJitEntry, CJitStateEnum, IBtBolt11Invoice, IGift};
1717
use crate::onchain::{AddressError, ValidationResult, GetAddressResponse, Network, GetAddressesResponse};
1818
pub use crate::onchain::WordCount;
@@ -401,6 +401,51 @@ pub fn get_all_unique_tags() -> Result<Vec<String>, ActivityError> {
401401
db.get_all_unique_tags()
402402
}
403403

404+
#[uniffi::export]
405+
pub fn insert_closed_channel(channel: ClosedChannelDetails) -> Result<(), ActivityError> {
406+
let mut guard = get_activity_db()?;
407+
let db = guard.activity_db.as_mut().ok_or(ActivityError::ConnectionError {
408+
error_details: "Database not initialized. Call init_db first.".to_string()
409+
})?;
410+
db.insert_closed_channel(&channel)
411+
}
412+
413+
#[uniffi::export]
414+
pub fn get_closed_channel_by_id(channel_id: String) -> Result<Option<ClosedChannelDetails>, ActivityError> {
415+
let guard = get_activity_db()?;
416+
let db = guard.activity_db.as_ref().ok_or(ActivityError::ConnectionError {
417+
error_details: "Database not initialized. Call init_db first.".to_string()
418+
})?;
419+
db.get_closed_channel_by_id(&channel_id)
420+
}
421+
422+
#[uniffi::export]
423+
pub fn get_all_closed_channels(sort_direction: Option<SortDirection>) -> Result<Vec<ClosedChannelDetails>, ActivityError> {
424+
let guard = get_activity_db()?;
425+
let db = guard.activity_db.as_ref().ok_or(ActivityError::ConnectionError {
426+
error_details: "Database not initialized. Call init_db first.".to_string()
427+
})?;
428+
db.get_all_closed_channels(sort_direction)
429+
}
430+
431+
#[uniffi::export]
432+
pub fn remove_closed_channel_by_id(channel_id: String) -> Result<bool, ActivityError> {
433+
let mut guard = get_activity_db()?;
434+
let db = guard.activity_db.as_mut().ok_or(ActivityError::ConnectionError {
435+
error_details: "Database not initialized. Call init_db first.".to_string()
436+
})?;
437+
db.remove_closed_channel_by_id(&channel_id)
438+
}
439+
440+
#[uniffi::export]
441+
pub fn wipe_all_closed_channels() -> Result<(), ActivityError> {
442+
let mut guard = get_activity_db()?;
443+
let db = guard.activity_db.as_mut().ok_or(ActivityError::ConnectionError {
444+
error_details: "Database not initialized. Call init_db first.".to_string()
445+
})?;
446+
db.wipe_all_closed_channels()
447+
}
448+
404449
#[uniffi::export]
405450
pub async fn update_blocktank_url(new_url: String) -> Result<(), BlocktankError> {
406451
let rt = ensure_runtime();

src/modules/activity/implementation.rs

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use rusqlite::{Connection, OptionalExtension};
2-
use crate::activity::{Activity, ActivityError, ActivityFilter, LightningActivity, OnchainActivity, PaymentState, PaymentType, SortDirection};
2+
use crate::activity::{Activity, ActivityError, ActivityFilter, LightningActivity, OnchainActivity, PaymentState, PaymentType, SortDirection, ClosedChannelDetails};
33

44
pub struct ActivityDB {
55
pub conn: Connection,
@@ -62,6 +62,24 @@ const CREATE_TAGS_TABLE: &str = "
6262
ON DELETE CASCADE
6363
)";
6464

65+
const CREATE_CLOSED_CHANNELS_TABLE: &str = "
66+
CREATE TABLE IF NOT EXISTS closed_channels (
67+
channel_id TEXT PRIMARY KEY,
68+
counterparty_node_id TEXT NOT NULL,
69+
funding_txo_txid TEXT NOT NULL,
70+
funding_txo_index INTEGER NOT NULL CHECK (funding_txo_index >= 0),
71+
channel_value_sats INTEGER NOT NULL CHECK (channel_value_sats >= 0),
72+
closed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
73+
outbound_capacity_msat INTEGER NOT NULL CHECK (outbound_capacity_msat >= 0),
74+
inbound_capacity_msat INTEGER NOT NULL CHECK (inbound_capacity_msat >= 0),
75+
counterparty_unspendable_punishment_reserve INTEGER NOT NULL CHECK (counterparty_unspendable_punishment_reserve >= 0),
76+
unspendable_punishment_reserve INTEGER NOT NULL CHECK (unspendable_punishment_reserve >= 0),
77+
forwarding_fee_proportional_millionths INTEGER NOT NULL CHECK (forwarding_fee_proportional_millionths >= 0),
78+
forwarding_fee_base_msat INTEGER NOT NULL CHECK (forwarding_fee_base_msat >= 0),
79+
channel_name TEXT NOT NULL,
80+
channel_closure_reason TEXT NOT NULL
81+
)";
82+
6583
const INDEX_STATEMENTS: &[&str] = &[
6684
// Activity indexes
6785
"CREATE INDEX IF NOT EXISTS idx_activities_type_timestamp ON activities(activity_type, timestamp DESC)",
@@ -77,7 +95,10 @@ const INDEX_STATEMENTS: &[&str] = &[
7795
"CREATE UNIQUE INDEX IF NOT EXISTS idx_lightning_id ON lightning_activity(id)",
7896

7997
// Tags indexes
80-
"CREATE INDEX IF NOT EXISTS idx_activity_tags_tag_activity ON activity_tags(tag, activity_id)"
98+
"CREATE INDEX IF NOT EXISTS idx_activity_tags_tag_activity ON activity_tags(tag, activity_id)",
99+
100+
// Closed channels indexes
101+
"CREATE INDEX IF NOT EXISTS idx_closed_channels_funding_txo ON closed_channels(funding_txo_txid)"
81102
];
82103

83104
const TRIGGER_STATEMENTS: &[&str] = &[
@@ -181,6 +202,13 @@ impl ActivityDB {
181202
});
182203
}
183204

205+
// Create closed channels table
206+
if let Err(e) = self.conn.execute(CREATE_CLOSED_CHANNELS_TABLE, []) {
207+
return Err(ActivityError::InitializationError {
208+
error_details: format!("Error creating closed_channels table: {}", e),
209+
});
210+
}
211+
184212
// Create indexes
185213
for statement in INDEX_STATEMENTS {
186214
if let Err(e) = self.conn.execute(statement, []) {
@@ -991,6 +1019,148 @@ impl ActivityDB {
9911019
Ok(tags)
9921020
}
9931021

1022+
pub fn insert_closed_channel(&mut self, channel: &ClosedChannelDetails) -> Result<(), ActivityError> {
1023+
if channel.channel_id.is_empty() {
1024+
return Err(ActivityError::DataError {
1025+
error_details: "Channel ID cannot be empty".to_string(),
1026+
});
1027+
}
1028+
1029+
let sql = "
1030+
INSERT INTO closed_channels (
1031+
channel_id, counterparty_node_id, funding_txo_txid, funding_txo_index,
1032+
channel_value_sats, closed_at, outbound_capacity_msat, inbound_capacity_msat,
1033+
counterparty_unspendable_punishment_reserve, unspendable_punishment_reserve,
1034+
forwarding_fee_proportional_millionths, forwarding_fee_base_msat,
1035+
channel_name, channel_closure_reason
1036+
) VALUES (
1037+
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14
1038+
)";
1039+
1040+
self.conn.execute(
1041+
sql,
1042+
rusqlite::params![
1043+
&channel.channel_id,
1044+
&channel.counterparty_node_id,
1045+
&channel.funding_txo_txid,
1046+
channel.funding_txo_index as i64,
1047+
channel.channel_value_sats as i64,
1048+
channel.closed_at as i64,
1049+
channel.outbound_capacity_msat as i64,
1050+
channel.inbound_capacity_msat as i64,
1051+
channel.counterparty_unspendable_punishment_reserve as i64,
1052+
channel.unspendable_punishment_reserve as i64,
1053+
channel.forwarding_fee_proportional_millionths as i64,
1054+
channel.forwarding_fee_base_msat as i64,
1055+
&channel.channel_name,
1056+
&channel.channel_closure_reason,
1057+
],
1058+
).map_err(|e| ActivityError::InsertError {
1059+
error_details: format!("Failed to insert closed channel: {}", e),
1060+
})?;
1061+
1062+
Ok(())
1063+
}
1064+
1065+
pub fn get_closed_channel_by_id(&self, channel_id: &str) -> Result<Option<ClosedChannelDetails>, ActivityError> {
1066+
let sql = "
1067+
SELECT
1068+
channel_id, counterparty_node_id, funding_txo_txid, funding_txo_index,
1069+
channel_value_sats, closed_at, outbound_capacity_msat, inbound_capacity_msat,
1070+
counterparty_unspendable_punishment_reserve, unspendable_punishment_reserve,
1071+
forwarding_fee_proportional_millionths, forwarding_fee_base_msat,
1072+
channel_name, channel_closure_reason
1073+
FROM closed_channels
1074+
WHERE channel_id = ?1";
1075+
1076+
let mut stmt = self.conn.prepare(sql).map_err(|e| ActivityError::RetrievalError {
1077+
error_details: format!("Failed to prepare statement: {}", e),
1078+
})?;
1079+
1080+
match stmt.query_row([channel_id], |row| {
1081+
let channel_value_sats: i64 = row.get(4)?;
1082+
let outbound_capacity_msat: i64 = row.get(6)?;
1083+
let inbound_capacity_msat: i64 = row.get(7)?;
1084+
let counterparty_unspendable_punishment_reserve: i64 = row.get(8)?;
1085+
1086+
Ok(ClosedChannelDetails {
1087+
channel_id: row.get(0)?,
1088+
counterparty_node_id: row.get(1)?,
1089+
funding_txo_txid: row.get(2)?,
1090+
funding_txo_index: row.get::<_, i64>(3)? as u32,
1091+
channel_value_sats: channel_value_sats as u64,
1092+
closed_at: row.get::<_, i64>(5)? as u64,
1093+
outbound_capacity_msat: outbound_capacity_msat as u64,
1094+
inbound_capacity_msat: inbound_capacity_msat as u64,
1095+
counterparty_unspendable_punishment_reserve: counterparty_unspendable_punishment_reserve as u64,
1096+
unspendable_punishment_reserve: row.get::<_, i64>(9)? as u64,
1097+
forwarding_fee_proportional_millionths: row.get::<_, i64>(10)? as u32,
1098+
forwarding_fee_base_msat: row.get::<_, i64>(11)? as u32,
1099+
channel_name: row.get(12)?,
1100+
channel_closure_reason: row.get(13)?,
1101+
})
1102+
}) {
1103+
Ok(channel) => Ok(Some(channel)),
1104+
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
1105+
Err(e) => Err(ActivityError::RetrievalError {
1106+
error_details: format!("Failed to get closed channel: {}", e),
1107+
}),
1108+
}
1109+
}
1110+
1111+
pub fn get_all_closed_channels(&self, sort_direction: Option<SortDirection>) -> Result<Vec<ClosedChannelDetails>, ActivityError> {
1112+
let direction = sort_direction.unwrap_or_default();
1113+
let sql = format!(
1114+
"
1115+
SELECT
1116+
channel_id, counterparty_node_id, funding_txo_txid, funding_txo_index,
1117+
channel_value_sats, closed_at, outbound_capacity_msat, inbound_capacity_msat,
1118+
counterparty_unspendable_punishment_reserve, unspendable_punishment_reserve,
1119+
forwarding_fee_proportional_millionths, forwarding_fee_base_msat,
1120+
channel_name, channel_closure_reason
1121+
FROM closed_channels
1122+
ORDER BY closed_at {}
1123+
",
1124+
Self::sort_direction_to_sql(direction)
1125+
);
1126+
1127+
let mut stmt = self.conn.prepare(&sql).map_err(|e| ActivityError::RetrievalError {
1128+
error_details: format!("Failed to prepare statement: {}", e),
1129+
})?;
1130+
1131+
let channels = stmt.query_map([], |row| {
1132+
let channel_value_sats: i64 = row.get(4)?;
1133+
let outbound_capacity_msat: i64 = row.get(6)?;
1134+
let inbound_capacity_msat: i64 = row.get(7)?;
1135+
let counterparty_unspendable_punishment_reserve: i64 = row.get(8)?;
1136+
1137+
Ok(ClosedChannelDetails {
1138+
channel_id: row.get(0)?,
1139+
counterparty_node_id: row.get(1)?,
1140+
funding_txo_txid: row.get(2)?,
1141+
funding_txo_index: row.get::<_, i64>(3)? as u32,
1142+
channel_value_sats: channel_value_sats as u64,
1143+
closed_at: row.get::<_, i64>(5)? as u64,
1144+
outbound_capacity_msat: outbound_capacity_msat as u64,
1145+
inbound_capacity_msat: inbound_capacity_msat as u64,
1146+
counterparty_unspendable_punishment_reserve: counterparty_unspendable_punishment_reserve as u64,
1147+
unspendable_punishment_reserve: row.get::<_, i64>(9)? as u64,
1148+
forwarding_fee_proportional_millionths: row.get::<_, i64>(10)? as u32,
1149+
forwarding_fee_base_msat: row.get::<_, i64>(11)? as u32,
1150+
channel_name: row.get(12)?,
1151+
channel_closure_reason: row.get(13)?,
1152+
})
1153+
}).map_err(|e| ActivityError::RetrievalError {
1154+
error_details: format!("Failed to execute query: {}", e),
1155+
})?
1156+
.collect::<Result<Vec<ClosedChannelDetails>, _>>()
1157+
.map_err(|e| ActivityError::DataError {
1158+
error_details: format!("Failed to process rows: {}", e),
1159+
})?;
1160+
1161+
Ok(channels)
1162+
}
1163+
9941164
/// Helper function to convert PaymentType to string
9951165
fn payment_type_to_string(payment_type: &PaymentType) -> &'static str {
9961166
match payment_type {
@@ -1043,6 +1213,27 @@ impl ActivityDB {
10431213
}
10441214
}
10451215

1216+
/// Wipes all closed channels from the database
1217+
pub fn wipe_all_closed_channels(&mut self) -> Result<(), ActivityError> {
1218+
self.conn.execute("DELETE FROM closed_channels", [])
1219+
.map_err(|e| ActivityError::DataError {
1220+
error_details: format!("Failed to delete all closed channels: {}", e),
1221+
})?;
1222+
1223+
Ok(())
1224+
}
1225+
1226+
pub fn remove_closed_channel_by_id(&mut self, channel_id: &str) -> Result<bool, ActivityError> {
1227+
let rows = self.conn.execute(
1228+
"DELETE FROM closed_channels WHERE channel_id = ?1",
1229+
[channel_id],
1230+
).map_err(|e| ActivityError::DataError {
1231+
error_details: format!("Failed to delete closed channel: {}", e),
1232+
})?;
1233+
1234+
Ok(rows > 0)
1235+
}
1236+
10461237
/// Wipes all activity data from the database
10471238
/// This deletes all activities, which cascades to delete all tags due to foreign key constraints
10481239
pub fn wipe_all(&mut self) -> Result<(), ActivityError> {
@@ -1056,6 +1247,12 @@ impl ActivityDB {
10561247
error_details: format!("Failed to delete all activities: {}", e),
10571248
})?;
10581249

1250+
// Delete all closed channels
1251+
tx.execute("DELETE FROM closed_channels", [])
1252+
.map_err(|e| ActivityError::DataError {
1253+
error_details: format!("Failed to delete all closed channels: {}", e),
1254+
})?;
1255+
10591256
tx.commit().map_err(|e| ActivityError::DataError {
10601257
error_details: format!("Failed to commit transaction: {}", e),
10611258
})?;

0 commit comments

Comments
 (0)