Skip to content

Commit 1175f20

Browse files
committed
feat: add tx details table
1 parent 847266a commit 1175f20

File tree

4 files changed

+515
-3
lines changed

4 files changed

+515
-3
lines changed

src/lib.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub use modules::scanner::{
1010
pub use modules::lnurl;
1111
pub use modules::onchain;
1212
pub use modules::activity;
13-
use crate::activity::{ActivityError, ActivityDB, OnchainActivity, LightningActivity, Activity, ActivityFilter, SortDirection, PaymentType, DbError, ClosedChannelDetails, ActivityTags, PreActivityMetadata};
13+
use crate::activity::{ActivityError, ActivityDB, OnchainActivity, LightningActivity, Activity, ActivityFilter, SortDirection, PaymentType, DbError, ClosedChannelDetails, ActivityTags, PreActivityMetadata, TransactionDetails, TxInput, TxOutput};
1414
use crate::modules::blocktank::{BlocktankDB, BlocktankError, IBtInfo, IBtOrder, CreateOrderOptions, BtOrderState2, IBt0ConfMinTxFeeWindow, IBtEstimateFeeResponse, IBtEstimateFeeResponse2, CreateCjitOptions, ICJitEntry, CJitStateEnum, IBtBolt11Invoice, IGift, ChannelLiquidityOptions, ChannelLiquidityParams, DefaultLspBalanceParams};
1515
use crate::onchain::{AddressError, ValidationResult, GetAddressResponse, Network, GetAddressesResponse};
1616
pub use crate::onchain::WordCount;
@@ -1444,6 +1444,51 @@ pub fn mark_activity_as_seen(activity_id: String, seen_at: u64) -> Result<(), Ac
14441444
db.mark_activity_as_seen(&activity_id, seen_at)
14451445
}
14461446

1447+
#[uniffi::export]
1448+
pub fn upsert_transaction_details(details_list: Vec<TransactionDetails>) -> Result<(), ActivityError> {
1449+
let mut guard = get_activity_db()?;
1450+
let db = guard.activity_db.as_mut().ok_or(ActivityError::ConnectionError {
1451+
error_details: "Database not initialized. Call init_db first.".to_string()
1452+
})?;
1453+
db.upsert_transaction_details(&details_list)
1454+
}
1455+
1456+
#[uniffi::export]
1457+
pub fn get_transaction_details(tx_id: String) -> Result<Option<TransactionDetails>, ActivityError> {
1458+
let guard = get_activity_db()?;
1459+
let db = guard.activity_db.as_ref().ok_or(ActivityError::ConnectionError {
1460+
error_details: "Database not initialized. Call init_db first.".to_string()
1461+
})?;
1462+
db.get_transaction_details(&tx_id)
1463+
}
1464+
1465+
#[uniffi::export]
1466+
pub fn get_all_transaction_details() -> Result<Vec<TransactionDetails>, ActivityError> {
1467+
let guard = get_activity_db()?;
1468+
let db = guard.activity_db.as_ref().ok_or(ActivityError::ConnectionError {
1469+
error_details: "Database not initialized. Call init_db first.".to_string()
1470+
})?;
1471+
db.get_all_transaction_details()
1472+
}
1473+
1474+
#[uniffi::export]
1475+
pub fn delete_transaction_details(tx_id: String) -> Result<bool, ActivityError> {
1476+
let mut guard = get_activity_db()?;
1477+
let db = guard.activity_db.as_mut().ok_or(ActivityError::ConnectionError {
1478+
error_details: "Database not initialized. Call init_db first.".to_string()
1479+
})?;
1480+
db.delete_transaction_details(&tx_id)
1481+
}
1482+
1483+
#[uniffi::export]
1484+
pub fn wipe_all_transaction_details() -> Result<(), ActivityError> {
1485+
let mut guard = get_activity_db()?;
1486+
let db = guard.activity_db.as_mut().ok_or(ActivityError::ConnectionError {
1487+
error_details: "Database not initialized. Call init_db first.".to_string()
1488+
})?;
1489+
db.wipe_all_transaction_details()
1490+
}
1491+
14471492
#[uniffi::export]
14481493
pub async fn blocktank_remove_all_orders() -> Result<(), BlocktankError> {
14491494
let rt = ensure_runtime();

src/modules/activity/implementation.rs

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use rusqlite::{Connection, OptionalExtension};
22
use serde_json;
3-
use crate::activity::{Activity, ActivityError, ActivityFilter, LightningActivity, OnchainActivity, PaymentState, PaymentType, SortDirection, ClosedChannelDetails, ActivityTags, PreActivityMetadata};
3+
use crate::activity::{Activity, ActivityError, ActivityFilter, LightningActivity, OnchainActivity, PaymentState, PaymentType, SortDirection, ClosedChannelDetails, ActivityTags, PreActivityMetadata, TransactionDetails, TxInput, TxOutput};
44

55
pub struct ActivityDB {
66
pub conn: Connection,
@@ -96,6 +96,14 @@ const CREATE_CLOSED_CHANNELS_TABLE: &str = "
9696
channel_closure_reason TEXT NOT NULL
9797
)";
9898

99+
const CREATE_TRANSACTION_DETAILS_TABLE: &str = "
100+
CREATE TABLE IF NOT EXISTS transaction_details (
101+
tx_id TEXT PRIMARY KEY,
102+
amount_sats INTEGER NOT NULL,
103+
inputs TEXT NOT NULL,
104+
outputs TEXT NOT NULL
105+
)";
106+
99107
const UPSERT_CLOSED_CHANNEL_SQL: &str = "
100108
INSERT OR REPLACE INTO closed_channels (
101109
channel_id, counterparty_node_id, funding_txo_txid, funding_txo_index,
@@ -248,6 +256,13 @@ impl ActivityDB {
248256
});
249257
}
250258

259+
// Create transaction details table
260+
if let Err(e) = self.conn.execute(CREATE_TRANSACTION_DETAILS_TABLE, []) {
261+
return Err(ActivityError::InitializationError {
262+
error_details: format!("Error creating transaction_details table: {}", e),
263+
});
264+
}
265+
251266
// Create indexes
252267
for statement in INDEX_STATEMENTS {
253268
if let Err(e) = self.conn.execute(statement, []) {
@@ -2051,6 +2066,157 @@ impl ActivityDB {
20512066
Ok(rows > 0)
20522067
}
20532068

2069+
/// Upserts transaction details for one or more onchain transactions.
2070+
pub fn upsert_transaction_details(&mut self, details_list: &[TransactionDetails]) -> Result<(), ActivityError> {
2071+
if details_list.is_empty() {
2072+
return Ok(());
2073+
}
2074+
2075+
let tx = self.conn.transaction().map_err(|e| ActivityError::DataError {
2076+
error_details: format!("Failed to start transaction: {}", e),
2077+
})?;
2078+
2079+
{
2080+
let mut stmt = tx.prepare(
2081+
"INSERT OR REPLACE INTO transaction_details (tx_id, amount_sats, inputs, outputs) VALUES (?1, ?2, ?3, ?4)"
2082+
).map_err(|e| ActivityError::DataError {
2083+
error_details: format!("Failed to prepare statement: {}", e),
2084+
})?;
2085+
2086+
for details in details_list {
2087+
if details.tx_id.is_empty() {
2088+
return Err(ActivityError::DataError {
2089+
error_details: "Transaction ID cannot be empty".to_string(),
2090+
});
2091+
}
2092+
2093+
let inputs_json = serde_json::to_string(&details.inputs).map_err(|e| ActivityError::DataError {
2094+
error_details: format!("Failed to serialize inputs: {}", e),
2095+
})?;
2096+
2097+
let outputs_json = serde_json::to_string(&details.outputs).map_err(|e| ActivityError::DataError {
2098+
error_details: format!("Failed to serialize outputs: {}", e),
2099+
})?;
2100+
2101+
stmt.execute(rusqlite::params![
2102+
&details.tx_id,
2103+
details.amount_sats,
2104+
&inputs_json,
2105+
&outputs_json,
2106+
]).map_err(|e| ActivityError::InsertError {
2107+
error_details: format!("Failed to upsert transaction details: {}", e),
2108+
})?;
2109+
}
2110+
}
2111+
2112+
tx.commit().map_err(|e| ActivityError::DataError {
2113+
error_details: format!("Failed to commit transaction: {}", e),
2114+
})?;
2115+
2116+
Ok(())
2117+
}
2118+
2119+
/// Retrieves transaction details by transaction ID.
2120+
pub fn get_transaction_details(&self, tx_id: &str) -> Result<Option<TransactionDetails>, ActivityError> {
2121+
let sql = "SELECT tx_id, amount_sats, inputs, outputs FROM transaction_details WHERE tx_id = ?1";
2122+
2123+
let mut stmt = self.conn.prepare(sql).map_err(|e| ActivityError::RetrievalError {
2124+
error_details: format!("Failed to prepare statement: {}", e),
2125+
})?;
2126+
2127+
match stmt.query_row([tx_id], |row| {
2128+
let tx_id: String = row.get(0)?;
2129+
let amount_sats: i64 = row.get(1)?;
2130+
let inputs_json: String = row.get(2)?;
2131+
let outputs_json: String = row.get(3)?;
2132+
2133+
let inputs: Vec<TxInput> = serde_json::from_str(&inputs_json).map_err(|_| {
2134+
rusqlite::Error::InvalidColumnType(2, "inputs".to_string(), rusqlite::types::Type::Text)
2135+
})?;
2136+
2137+
let outputs: Vec<TxOutput> = serde_json::from_str(&outputs_json).map_err(|_| {
2138+
rusqlite::Error::InvalidColumnType(3, "outputs".to_string(), rusqlite::types::Type::Text)
2139+
})?;
2140+
2141+
Ok(TransactionDetails {
2142+
tx_id,
2143+
amount_sats,
2144+
inputs,
2145+
outputs,
2146+
})
2147+
}) {
2148+
Ok(details) => Ok(Some(details)),
2149+
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
2150+
Err(e) => Err(ActivityError::RetrievalError {
2151+
error_details: format!("Failed to get transaction details: {}", e),
2152+
}),
2153+
}
2154+
}
2155+
2156+
/// Retrieves all transaction details.
2157+
pub fn get_all_transaction_details(&self) -> Result<Vec<TransactionDetails>, ActivityError> {
2158+
let sql = "SELECT tx_id, amount_sats, inputs, outputs FROM transaction_details ORDER BY tx_id";
2159+
2160+
let mut stmt = self.conn.prepare(sql).map_err(|e| ActivityError::RetrievalError {
2161+
error_details: format!("Failed to prepare statement: {}", e),
2162+
})?;
2163+
2164+
let rows = stmt.query_map([], |row| {
2165+
let tx_id: String = row.get(0)?;
2166+
let amount_sats: i64 = row.get(1)?;
2167+
let inputs_json: String = row.get(2)?;
2168+
let outputs_json: String = row.get(3)?;
2169+
2170+
let inputs: Vec<TxInput> = serde_json::from_str(&inputs_json).map_err(|_| {
2171+
rusqlite::Error::InvalidColumnType(2, "inputs".to_string(), rusqlite::types::Type::Text)
2172+
})?;
2173+
2174+
let outputs: Vec<TxOutput> = serde_json::from_str(&outputs_json).map_err(|_| {
2175+
rusqlite::Error::InvalidColumnType(3, "outputs".to_string(), rusqlite::types::Type::Text)
2176+
})?;
2177+
2178+
Ok(TransactionDetails {
2179+
tx_id,
2180+
amount_sats,
2181+
inputs,
2182+
outputs,
2183+
})
2184+
}).map_err(|e| ActivityError::RetrievalError {
2185+
error_details: format!("Failed to execute query: {}", e),
2186+
})?;
2187+
2188+
let mut results = Vec::new();
2189+
for row in rows {
2190+
results.push(row.map_err(|e| ActivityError::DataError {
2191+
error_details: format!("Failed to process row: {}", e),
2192+
})?);
2193+
}
2194+
2195+
Ok(results)
2196+
}
2197+
2198+
/// Deletes transaction details by transaction ID.
2199+
pub fn delete_transaction_details(&mut self, tx_id: &str) -> Result<bool, ActivityError> {
2200+
let rows = self.conn.execute(
2201+
"DELETE FROM transaction_details WHERE tx_id = ?1",
2202+
[tx_id],
2203+
).map_err(|e| ActivityError::DataError {
2204+
error_details: format!("Failed to delete transaction details: {}", e),
2205+
})?;
2206+
2207+
Ok(rows > 0)
2208+
}
2209+
2210+
/// Wipes all transaction details from the database.
2211+
pub fn wipe_all_transaction_details(&mut self) -> Result<(), ActivityError> {
2212+
self.conn.execute("DELETE FROM transaction_details", [])
2213+
.map_err(|e| ActivityError::DataError {
2214+
error_details: format!("Failed to delete all transaction details: {}", e),
2215+
})?;
2216+
2217+
Ok(())
2218+
}
2219+
20542220
/// Wipes all activity data from the database
20552221
/// This deletes all activities, which cascades to delete all activity_tags due to foreign key constraints.
20562222
/// Also deletes all pre_activity_metadata and closed_channels.
@@ -2077,6 +2243,12 @@ impl ActivityDB {
20772243
error_details: format!("Failed to delete all closed channels: {}", e),
20782244
})?;
20792245

2246+
// Delete all transaction details
2247+
tx.execute("DELETE FROM transaction_details", [])
2248+
.map_err(|e| ActivityError::DataError {
2249+
error_details: format!("Failed to delete all transaction details: {}", e),
2250+
})?;
2251+
20802252
tx.commit().map_err(|e| ActivityError::DataError {
20812253
error_details: format!("Failed to commit transaction: {}", e),
20822254
})?;

0 commit comments

Comments
 (0)