From 27e0763fd4bb964985873aef51efe8a407ef3323 Mon Sep 17 00:00:00 2001 From: user Date: Wed, 16 Jul 2025 13:18:15 -0400 Subject: [PATCH] Add insert_inputs_seen_before database method liana did not have a way to add coins from external wallets to the db, this adds those coins to the db and forces is_from_self to be false and sets the wallet_id to 2 which is never the users wallet as it is hardcoded to 1. --- lianad/src/database/mod.rs | 7 ++++++ lianad/src/database/sqlite/mod.rs | 30 +++++++++++++++++++++++++- lianad/src/database/sqlite/schema.rs | 32 ++++++++++++++++++++++++++++ lianad/src/payjoin/receiver.rs | 19 ++++++++++++----- 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index 519b2ee72..108f24cae 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -239,6 +239,9 @@ pub trait DatabaseConnection { /// Load all receiver session events for a particular session id fn load_receiver_session_events(&mut self, session_id: &SessionId) -> Vec>; + /// Check if input has been seen before and then add it to the input_seen table + fn insert_input_seen_before(&mut self, outpoints: &[bitcoin::OutPoint]) -> bool; + /// Create a payjoin sender fn save_new_payjoin_sender_session(&mut self, original_txid: &bitcoin::Txid) -> i64; /// Get a all active payjoin senders @@ -484,6 +487,10 @@ impl DatabaseConnection for SqliteConn { .collect() } + fn insert_input_seen_before(&mut self, outpoints: &[bitcoin::OutPoint]) -> bool { + self.insert_outpoint_seen_before(outpoints) + } + fn payjoin_get_ohttp_keys(&mut self, ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { self.payjoin_get_ohttp_keys(ohttp_relay) } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 6c5f286de..891620dc5 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -28,7 +28,7 @@ use crate::{ payjoin::db::SessionId, }; use liana::descriptors::LianaDescriptor; -use payjoin::OhttpKeys; +use payjoin::{bitcoin::consensus::Encodable, OhttpKeys}; use std::{ cmp, @@ -481,6 +481,34 @@ impl SqliteConn { .expect("Database must be available") } + pub fn insert_outpoint_seen_before<'a>( + &mut self, + outpoints: impl IntoIterator, + ) -> bool { + let mut is_duplicate = false; + db_exec(&mut self.conn, |db_tx| { + for outpoint in outpoints { + let mut buf = Vec::new(); + outpoint + .consensus_encode(&mut buf) + .expect("Outpoint must encode"); + let affected = db_tx.execute( + "INSERT OR IGNORE INTO payjoin_outpoints (outpoint, added_at) \ + VALUES (?1, ?2)", + rusqlite::params![buf, curr_timestamp()], + )?; + + if affected == 0 { + is_duplicate = true + } + } + Ok(()) + }) + .expect("database must be available"); + + is_duplicate + } + /// Remove a set of coins from the database. pub fn remove_coins(&mut self, outpoints: &[bitcoin::OutPoint]) { db_exec(&mut self.conn, |db_tx| { diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index eddf2f03b..d5f399164 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -1,5 +1,6 @@ use bip329::Label; use liana::descriptors::LianaDescriptor; +use payjoin::bitcoin::{consensus::Decodable, io::Cursor}; use std::{convert::TryFrom, str::FromStr}; @@ -87,6 +88,16 @@ CREATE TABLE coins ( ON DELETE RESTRICT ); +/* Seen Payjoin outpoints + * + * The 'added_at' field is simply the time that this outpoint is added to the table for + * tracking. + */ +CREATE TABLE payjoin_outpoints ( + outpoint BLOB NOT NULL PRIMARY KEY, + added_at INTEGER NOT NULL +); + /* A mapping from descriptor address to derivation index. Necessary until * we can get the derivation index from the parent descriptor from bitcoind. */ @@ -500,3 +511,24 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWalletTransaction { }) } } + +/// An outpoint we have seen before in payjoin transactions +#[derive(Clone, Debug, PartialEq)] +pub struct DbPayjoinOutpoint { + pub outpoint: bitcoin::OutPoint, + pub added_at: Option, +} + +impl TryFrom<&rusqlite::Row<'_>> for DbPayjoinOutpoint { + type Error = rusqlite::Error; + + fn try_from(row: &rusqlite::Row) -> Result { + let outpoint: Vec = row.get(0)?; + let outpoint = bitcoin::OutPoint::consensus_decode(&mut Cursor::new(outpoint)) + .expect("Outpoint should be decodable"); + + let added_at = row.get(1)?; + + Ok(DbPayjoinOutpoint { outpoint, added_at }) + } +} diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 76c64d504..3ce3f5dc4 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -20,6 +20,7 @@ use payjoin::{ }, InputPair, }, + ImplementationError, }; use crate::{ @@ -91,7 +92,10 @@ fn check_inputs_not_owned( ) -> Result<(), Box> { let proposal = proposal .check_inputs_not_owned(&mut |script| { - let address = bitcoin::Address::from_script(script, db_conn.network()).unwrap(); + let address = + bitcoin::Address::from_script(script, db_conn.network()).map_err(|e| { + ImplementationError::from(Box::new(e) as Box) + })?; Ok(db_conn .derivation_index_by_address(&address) .map(|(index, is_change)| AddrInfo { index, is_change }) @@ -109,9 +113,10 @@ fn check_no_inputs_seen_before( secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal - // TODO implement check_no_inputs_seen_before callback and add new table to mark relevant - // outpoint as seen for the future - .check_no_inputs_seen_before(&mut |_| Ok(false)) + .check_no_inputs_seen_before(&mut |outpoint| { + let seen = db_conn.insert_input_seen_before(&[*outpoint]); + Ok(seen) + }) .save(persister)?; identify_receiver_outputs(proposal, persister, db_conn, desc, secp) } @@ -123,9 +128,13 @@ fn identify_receiver_outputs( desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { + log::debug!("[Payjoin] receiver outputs"); let proposal = proposal .identify_receiver_outputs(&mut |script| { - let address = bitcoin::Address::from_script(script, db_conn.network()).unwrap(); + let address = + bitcoin::Address::from_script(script, db_conn.network()).map_err(|e| { + ImplementationError::from(Box::new(e) as Box) + })?; Ok(db_conn .derivation_index_by_address(&address) .map(|(index, is_change)| AddrInfo { index, is_change })