1+ use alloc:: collections:: BTreeMap ;
2+
13use bdk_chain:: {
24 indexed_tx_graph, keychain_txout, local_chain, tx_graph, ConfirmationBlockTime , Merge ,
35} ;
6+ use bitcoin:: { OutPoint , Txid } ;
47use miniscript:: { Descriptor , DescriptorPublicKey } ;
58use serde:: { Deserialize , Serialize } ;
69
710type IndexedTxGraphChangeSet =
811 indexed_tx_graph:: ChangeSet < ConfirmationBlockTime , keychain_txout:: ChangeSet > ;
912
13+ use crate :: UtxoLock ;
14+
1015/// A change set for [`Wallet`]
1116///
1217/// ## Definition
@@ -114,6 +119,8 @@ pub struct ChangeSet {
114119 pub tx_graph : tx_graph:: ChangeSet < ConfirmationBlockTime > ,
115120 /// Changes to [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex).
116121 pub indexer : keychain_txout:: ChangeSet ,
122+ /// Changes to locked outpoints.
123+ pub locked_outpoints : BTreeMap < OutPoint , UtxoLock > ,
117124}
118125
119126impl Merge for ChangeSet {
@@ -142,6 +149,11 @@ impl Merge for ChangeSet {
142149 self . network = other. network ;
143150 }
144151
152+ // To merge `locked_outpoints` we extend the existing collection. If there's
153+ // an existing entry for a given outpoint, it is overwritten by the
154+ // new utxo lock.
155+ self . locked_outpoints . extend ( other. locked_outpoints ) ;
156+
145157 Merge :: merge ( & mut self . local_chain , other. local_chain ) ;
146158 Merge :: merge ( & mut self . tx_graph , other. tx_graph ) ;
147159 Merge :: merge ( & mut self . indexer , other. indexer ) ;
@@ -154,6 +166,7 @@ impl Merge for ChangeSet {
154166 && self . local_chain . is_empty ( )
155167 && self . tx_graph . is_empty ( )
156168 && self . indexer . is_empty ( )
169+ && self . locked_outpoints . is_empty ( )
157170 }
158171}
159172
@@ -163,6 +176,8 @@ impl ChangeSet {
163176 pub const WALLET_SCHEMA_NAME : & ' static str = "bdk_wallet" ;
164177 /// Name of table to store wallet descriptors and network.
165178 pub const WALLET_TABLE_NAME : & ' static str = "bdk_wallet" ;
179+ /// Name of table to store wallet locked outpoints.
180+ pub const WALLET_UTXO_LOCK_TABLE_NAME : & ' static str = "bdk_wallet_locked_outpoints" ;
166181
167182 /// Get v0 sqlite [ChangeSet] schema
168183 pub fn schema_v0 ( ) -> alloc:: string:: String {
@@ -177,12 +192,26 @@ impl ChangeSet {
177192 )
178193 }
179194
195+ /// Get v1 sqlite [`ChangeSet`] schema. Schema v1 adds a table for locked outpoints.
196+ pub fn schema_v1 ( ) -> alloc:: string:: String {
197+ format ! (
198+ "CREATE TABLE {} ( \
199+ txid TEXT NOT NULL, \
200+ vout INTEGER NOT NULL, \
201+ is_locked INTEGER, \
202+ expiration_height INTEGER, \
203+ PRIMARY KEY(txid, vout) \
204+ ) STRICT;",
205+ Self :: WALLET_UTXO_LOCK_TABLE_NAME ,
206+ )
207+ }
208+
180209 /// Initialize sqlite tables for wallet tables.
181210 pub fn init_sqlite_tables ( db_tx : & chain:: rusqlite:: Transaction ) -> chain:: rusqlite:: Result < ( ) > {
182211 crate :: rusqlite_impl:: migrate_schema (
183212 db_tx,
184213 Self :: WALLET_SCHEMA_NAME ,
185- & [ & Self :: schema_v0 ( ) ] ,
214+ & [ & Self :: schema_v0 ( ) , & Self :: schema_v1 ( ) ] ,
186215 ) ?;
187216
188217 bdk_chain:: local_chain:: ChangeSet :: init_sqlite_tables ( db_tx) ?;
@@ -220,6 +249,31 @@ impl ChangeSet {
220249 changeset. network = network. map ( Impl :: into_inner) ;
221250 }
222251
252+ // Select locked outpoints.
253+ let mut stmt = db_tx. prepare ( & format ! (
254+ "SELECT txid, vout, is_locked, expiration_height FROM {}" ,
255+ Self :: WALLET_UTXO_LOCK_TABLE_NAME ,
256+ ) ) ?;
257+ let rows = stmt. query_map ( [ ] , |row| {
258+ Ok ( (
259+ row. get :: < _ , Impl < Txid > > ( "txid" ) ?,
260+ row. get :: < _ , u32 > ( "vout" ) ?,
261+ row. get :: < _ , bool > ( "is_locked" ) ?,
262+ row. get :: < _ , Option < u32 > > ( "expiration_height" ) ?,
263+ ) )
264+ } ) ?;
265+ for row in rows {
266+ let ( Impl ( txid) , vout, is_locked, expiration_height) = row?;
267+ let utxo_lock = UtxoLock {
268+ outpoint : OutPoint :: new ( txid, vout) ,
269+ is_locked,
270+ expiration_height,
271+ } ;
272+ changeset
273+ . locked_outpoints
274+ . insert ( utxo_lock. outpoint , utxo_lock) ;
275+ }
276+
223277 changeset. local_chain = local_chain:: ChangeSet :: from_sqlite ( db_tx) ?;
224278 changeset. tx_graph = tx_graph:: ChangeSet :: < _ > :: from_sqlite ( db_tx) ?;
225279 changeset. indexer = keychain_txout:: ChangeSet :: from_sqlite ( db_tx) ?;
@@ -268,6 +322,21 @@ impl ChangeSet {
268322 } ) ?;
269323 }
270324
325+ // Insert locked outpoints.
326+ let mut stmt = db_tx. prepare_cached ( & format ! (
327+ "INSERT INTO {}(txid, vout, is_locked, expiration_height) VALUES(:txid, :vout, :is_locked, :expiration_height) ON CONFLICT DO UPDATE SET is_locked=:is_locked, expiration_height=:expiration_height" ,
328+ Self :: WALLET_UTXO_LOCK_TABLE_NAME ,
329+ ) ) ?;
330+ for ( & outpoint, utxo_lock) in & self . locked_outpoints {
331+ let OutPoint { txid, vout } = outpoint;
332+ stmt. execute ( named_params ! {
333+ ":txid" : Impl ( txid) ,
334+ ":vout" : vout,
335+ ":is_locked" : utxo_lock. is_locked,
336+ ":expiration_height" : utxo_lock. expiration_height,
337+ } ) ?;
338+ }
339+
271340 self . local_chain . persist_to_sqlite ( db_tx) ?;
272341 self . tx_graph . persist_to_sqlite ( db_tx) ?;
273342 self . indexer . persist_to_sqlite ( db_tx) ?;
0 commit comments