11use 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
44pub 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+
6583const 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
83104const 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