44
55// Standard library imports
66use std:: {
7+ collections:: BTreeMap ,
78 str:: FromStr ,
89 sync:: { Arc , OnceLock } ,
910} ;
@@ -236,7 +237,8 @@ impl Store<Postgres> {
236237 descriptor TEXT NOT NULL,
237238 descriptor_id BYTEA NOT NULL,
238239 last_revealed INTEGER DEFAULT 0,
239- PRIMARY KEY (wallet_name, keychainkind)
240+ PRIMARY KEY (wallet_name, keychainkind),
241+ UNIQUE (wallet_name, descriptor_id)
240242 )"# ,
241243 r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."block" (
242244 wallet_name TEXT NOT NULL,
@@ -272,6 +274,14 @@ impl Store<Postgres> {
272274 FOREIGN KEY (wallet_name, txid) REFERENCES "bdk_wallet"."tx"(wallet_name, txid)
273275 )"# ,
274276 r#"CREATE INDEX IF NOT EXISTS idx_anchor_tx_txid ON "bdk_wallet"."anchor_tx" (txid)"# ,
277+ r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."keychain_spk" (
278+ wallet_name TEXT NOT NULL,
279+ descriptor_id BYTEA NOT NULL,
280+ idx INTEGER NOT NULL,
281+ script BYTEA NOT NULL,
282+ PRIMARY KEY (wallet_name, descriptor_id, idx),
283+ FOREIGN KEY (wallet_name, descriptor_id) REFERENCES "bdk_wallet"."keychain"(wallet_name, descriptor_id)
284+ )"# ,
275285 ] ;
276286
277287 // Execute each query separately
@@ -310,8 +320,69 @@ impl Store<Postgres> {
310320 table : "alter tx table" . to_string ( ) ,
311321 source : e,
312322 } ) ?;
323+
324+ // Also add unique constraint and keychain_spk table for v2
325+ sqlx:: query (
326+ r#"ALTER TABLE "bdk_wallet"."keychain"
327+ ADD CONSTRAINT keychain_wallet_descriptor_unique
328+ UNIQUE (wallet_name, descriptor_id)"# ,
329+ )
330+ . execute ( & mut * tx)
331+ . await
332+ . map_err ( |e| BdkSqlxError :: QueryError {
333+ table : "add unique constraint to keychain" . to_string ( ) ,
334+ source : e,
335+ } ) ?;
336+
337+ sqlx:: query (
338+ r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."keychain_spk" (
339+ wallet_name TEXT NOT NULL,
340+ descriptor_id BYTEA NOT NULL,
341+ idx INTEGER NOT NULL,
342+ script BYTEA NOT NULL,
343+ PRIMARY KEY (wallet_name, descriptor_id, idx),
344+ FOREIGN KEY (wallet_name, descriptor_id) REFERENCES "bdk_wallet"."keychain"(wallet_name, descriptor_id)
345+ )"# ,
346+ )
347+ . execute ( & mut * tx)
348+ . await
349+ . map_err ( |e| BdkSqlxError :: QueryError {
350+ table : "create keychain_spk table" . to_string ( ) ,
351+ source : e,
352+ } ) ?;
313353 }
314- _ => { } // Fresh install or already at v2
354+ Some ( 2 ) => {
355+ // Migrate from v2 to v3: Add unique constraint and keychain_spk table
356+ sqlx:: query (
357+ r#"ALTER TABLE "bdk_wallet"."keychain"
358+ ADD CONSTRAINT IF NOT EXISTS keychain_wallet_descriptor_unique
359+ UNIQUE (wallet_name, descriptor_id)"# ,
360+ )
361+ . execute ( & mut * tx)
362+ . await
363+ . map_err ( |e| BdkSqlxError :: QueryError {
364+ table : "add unique constraint to keychain" . to_string ( ) ,
365+ source : e,
366+ } ) ?;
367+
368+ sqlx:: query (
369+ r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."keychain_spk" (
370+ wallet_name TEXT NOT NULL,
371+ descriptor_id BYTEA NOT NULL,
372+ idx INTEGER NOT NULL,
373+ script BYTEA NOT NULL,
374+ PRIMARY KEY (wallet_name, descriptor_id, idx),
375+ FOREIGN KEY (wallet_name, descriptor_id) REFERENCES "bdk_wallet"."keychain"(wallet_name, descriptor_id)
376+ )"# ,
377+ )
378+ . execute ( & mut * tx)
379+ . await
380+ . map_err ( |e| BdkSqlxError :: QueryError {
381+ table : "create keychain_spk table" . to_string ( ) ,
382+ source : e,
383+ } ) ?;
384+ }
385+ _ => { } // Fresh install or already at v3
315386 }
316387
317388 // Insert or update to current version
@@ -320,7 +391,7 @@ impl Store<Postgres> {
320391 VALUES ($1)
321392 ON CONFLICT (version) DO NOTHING"# ,
322393 )
323- . bind ( 2 ) // Current schema version is now 2
394+ . bind ( 3 ) // Current schema version is now 3
324395 . execute ( & mut * tx)
325396 . await
326397 . map_err ( |e| BdkSqlxError :: QueryError {
@@ -395,6 +466,8 @@ impl Store<Postgres> {
395466 if let Some ( last_rev) = external_last_revealed {
396467 changeset. indexer . last_revealed . insert ( did, last_rev as u32 ) ;
397468 }
469+ // Load SPK cache for external descriptor
470+ load_keychain_spks ( db_tx, wallet_name, did, changeset) . await ?;
398471 }
399472
400473 if let Some ( desc_str) = internal_desc_str {
@@ -404,6 +477,8 @@ impl Store<Postgres> {
404477 if let Some ( last_rev) = internal_last_revealed {
405478 changeset. indexer . last_revealed . insert ( did, last_rev as u32 ) ;
406479 }
480+ // Load SPK cache for internal descriptor
481+ load_keychain_spks ( db_tx, wallet_name, did, changeset) . await ?;
407482 }
408483
409484 changeset. tx_graph = tx_graph_changeset_from_postgres ( db_tx, wallet_name) . await ?;
@@ -440,6 +515,14 @@ impl Store<Postgres> {
440515 }
441516 }
442517
518+ // Persist SPK cache
519+ let spk_cache = & changeset. indexer . spk_cache ;
520+ if !spk_cache. is_empty ( ) {
521+ for ( desc_id, spks) in spk_cache {
522+ persist_keychain_spks ( & mut tx, wallet_name, * desc_id, spks) . await ?;
523+ }
524+ }
525+
443526 local_chain_changeset_persist_to_postgres ( & mut tx, wallet_name, & changeset. local_chain )
444527 . await ?;
445528 tx_graph_changeset_persist_to_postgres ( & mut tx, wallet_name, & changeset. tx_graph ) . await ?;
@@ -531,6 +614,74 @@ async fn update_last_revealed(
531614 Ok ( ( ) )
532615}
533616
617+ /// Load keychain script pubkeys from the database
618+ #[ tracing:: instrument( skip( db_tx, changeset) ) ]
619+ async fn load_keychain_spks (
620+ db_tx : & mut Transaction < ' _ , Postgres > ,
621+ wallet_name : & str ,
622+ descriptor_id : DescriptorId ,
623+ changeset : & mut ChangeSet ,
624+ ) -> Result < ( ) > {
625+ trace ! ( "load keychain spks" ) ;
626+
627+ let rows = sqlx:: query (
628+ r#"SELECT idx, script FROM "bdk_wallet"."keychain_spk"
629+ WHERE wallet_name = $1 AND descriptor_id = $2
630+ ORDER BY idx"# ,
631+ )
632+ . bind ( wallet_name)
633+ . bind ( descriptor_id. to_byte_array ( ) )
634+ . fetch_all ( & mut * * db_tx)
635+ . await
636+ . map_err ( |e| BdkSqlxError :: QueryError {
637+ table : "select keychain_spk" . to_string ( ) ,
638+ source : e,
639+ } ) ?;
640+
641+ if !rows. is_empty ( ) {
642+ let mut spks = BTreeMap :: new ( ) ;
643+ for row in rows {
644+ let idx: i32 = row. get ( "idx" ) ;
645+ let script: Vec < u8 > = row. get ( "script" ) ;
646+ spks. insert ( idx as u32 , ScriptBuf :: from_bytes ( script) ) ;
647+ }
648+ changeset. indexer . spk_cache . insert ( descriptor_id, spks) ;
649+ }
650+
651+ Ok ( ( ) )
652+ }
653+
654+ /// Persist keychain script pubkeys to the database
655+ #[ tracing:: instrument( skip( db_tx, spks) ) ]
656+ async fn persist_keychain_spks (
657+ db_tx : & mut Transaction < ' _ , Postgres > ,
658+ wallet_name : & str ,
659+ descriptor_id : DescriptorId ,
660+ spks : & BTreeMap < u32 , ScriptBuf > ,
661+ ) -> Result < ( ) > {
662+ trace ! ( "persist keychain spks" ) ;
663+
664+ for ( idx, spk) in spks {
665+ sqlx:: query (
666+ r#"INSERT INTO "bdk_wallet"."keychain_spk" (wallet_name, descriptor_id, idx, script)
667+ VALUES ($1, $2, $3, $4)
668+ ON CONFLICT (wallet_name, descriptor_id, idx) DO UPDATE SET script = $4"# ,
669+ )
670+ . bind ( wallet_name)
671+ . bind ( descriptor_id. to_byte_array ( ) )
672+ . bind ( * idx as i32 )
673+ . bind ( spk. as_bytes ( ) )
674+ . execute ( & mut * * db_tx)
675+ . await
676+ . map_err ( |e| BdkSqlxError :: QueryError {
677+ table : "insert keychain_spk" . to_string ( ) ,
678+ source : e,
679+ } ) ?;
680+ }
681+
682+ Ok ( ( ) )
683+ }
684+
534685/// Select transactions, txouts, and anchors.
535686#[ tracing:: instrument( skip( db_tx) ) ]
536687pub async fn tx_graph_changeset_from_postgres (
@@ -783,7 +934,7 @@ pub async fn local_chain_changeset_persist_to_postgres(
783934
784935/// Collects information on all the wallets in the database and dumps it to stdout.
785936#[ tracing:: instrument]
786- pub async fn easy_backup ( db : Pool < Postgres > ) -> Result < ( ) > {
937+ pub async fn _easy_backup ( db : Pool < Postgres > ) -> Result < ( ) > {
787938 trace ! ( "Starting easy backup" ) ;
788939
789940 let statement = r#"SELECT * FROM "bdk_wallet"."keychain""# ;
0 commit comments