1313//! This module provides helper functions and types to assist users in migrating wallet data
1414//! when upgrading between major versions of the `bdk_wallet` crate.
1515
16- #[ cfg( feature = "rusqlite" ) ]
1716use crate :: rusqlite:: { self , Connection } ;
18- #[ cfg( feature = "rusqlite" ) ]
19- use alloc:: { string:: String , string:: ToString , vec:: Vec } ;
17+ use crate :: KeychainKind :: { self , External , Internal } ;
18+ use alloc:: {
19+ string:: { FromUtf8Error , String , ToString } ,
20+ vec:: Vec ,
21+ } ;
22+ use core:: fmt;
2023
21- // pre-1.0 sqlite database migration helper functions
22-
23- /// `Pre1WalletKeychain` represents a structure that holds the keychain details
24+ /// [`PreV1WalletKeychain`] represents a structure that holds the keychain details
2425/// and metadata required for managing a wallet's keys.
25- #[ cfg( feature = "rusqlite" ) ]
2626#[ derive( Debug ) ]
27- pub struct Pre1WalletKeychain {
27+ pub struct PreV1WalletKeychain {
2828 /// The name of the wallet keychains, "External" or "Internal".
29- pub keychain : String ,
29+ pub keychain : KeychainKind ,
3030 /// The index of the last derived key in the wallet keychain.
3131 pub last_derivation_index : u32 ,
3232 /// Checksum of the keychain descriptor, it must match the corresponding post-1.0 bdk wallet
3333 /// descriptor checksum.
34- pub checksum : Vec < u8 > ,
34+ pub checksum : String ,
35+ }
36+
37+ /// Errors thrown when migrating from a pre-v1.0.0 BDK database.
38+ #[ derive( Debug ) ]
39+ pub enum PreV1MigrationError {
40+ /// A SQLite error
41+ RusqliteError ( rusqlite:: Error ) ,
42+ /// The keychain name is invalid, it must be "External" or "Internal"
43+ InvalidKeychain ( String ) ,
44+ /// The checksum could not be decoded as utf8
45+ InvalidChecksum ( FromUtf8Error ) ,
46+ }
47+
48+ impl fmt:: Display for PreV1MigrationError {
49+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
50+ match self {
51+ PreV1MigrationError :: RusqliteError ( e) => write ! ( f, "Rusqlite error: {}" , e) ,
52+ PreV1MigrationError :: InvalidKeychain ( e) => write ! ( f, "Invalid keychain path: {}" , e) ,
53+ PreV1MigrationError :: InvalidChecksum ( e) => write ! ( f, "Invalid checksum: {}" , e) ,
54+ }
55+ }
56+ }
57+
58+ impl std:: error:: Error for PreV1MigrationError { }
59+
60+ impl From < rusqlite:: Error > for PreV1MigrationError {
61+ fn from ( e : rusqlite:: Error ) -> Self {
62+ PreV1MigrationError :: RusqliteError ( e)
63+ }
3564}
3665
37- /// Retrieves a list of [`Pre1WalletKeychain `] objects from a pre-1 .0 bdk SQLite database.
66+ /// Retrieves a list of [`PreV1WalletKeychain `] objects from a pre-v1.0 .0 bdk SQLite database.
3867///
3968/// This function uses a connection to a pre-1.0 bdk wallet SQLite database to execute a query that
4069/// retrieves data from two tables (`last_derivation_indices` and `checksums`) and maps the
41- /// resulting rows to a list of `Pre1WalletKeychain` objects.
42- #[ cfg( feature = "rusqlite" ) ]
43- pub fn get_pre_1_wallet_keychains (
70+ /// resulting rows to a list of [`PreV1WalletKeychain`] objects.
71+ pub fn get_pre_v1_wallet_keychains (
4472 conn : & mut Connection ,
45- ) -> Result < Vec < Pre1WalletKeychain > , rusqlite :: Error > {
73+ ) -> Result < Vec < PreV1WalletKeychain > , PreV1MigrationError > {
4674 let db_tx = conn. transaction ( ) ?;
47- let mut statement = db_tx. prepare (
48- "SELECT idx.keychain AS keychain, value, checksum FROM last_derivation_indices AS idx \
75+ let mut statement = db_tx
76+ . prepare (
77+ "SELECT trim(idx.keychain,'\" ') AS keychain, value, checksum FROM last_derivation_indices AS idx \
4978 JOIN checksums AS chk ON idx.keychain = chk.keychain",
50- ) ?;
79+ ) ?;
5180 let row_iter = statement. query_map ( [ ] , |row| {
5281 Ok ( (
5382 row. get :: < _ , String > ( "keychain" ) ?,
@@ -58,8 +87,14 @@ pub fn get_pre_1_wallet_keychains(
5887 let mut keychains = vec ! [ ] ;
5988 for row in row_iter {
6089 let ( keychain, value, checksum) = row?;
61- keychains. push ( Pre1WalletKeychain {
62- keychain : keychain. trim_matches ( '"' ) . to_string ( ) ,
90+ let keychain = match keychain. as_str ( ) {
91+ "External" => Ok ( External ) ,
92+ "Internal" => Ok ( Internal ) ,
93+ name => Err ( PreV1MigrationError :: InvalidKeychain ( name. to_string ( ) ) ) ,
94+ } ?;
95+ let checksum = String :: from_utf8 ( checksum) . map_err ( PreV1MigrationError :: InvalidChecksum ) ?;
96+ keychains. push ( PreV1WalletKeychain {
97+ keychain,
6398 last_derivation_index : value,
6499 checksum,
65100 } )
@@ -69,50 +104,53 @@ pub fn get_pre_1_wallet_keychains(
69104
70105#[ cfg( test) ]
71106mod test {
72- #[ cfg( feature = "rusqlite" ) ]
73107 use crate :: rusqlite:: { self , Connection } ;
108+ use crate :: KeychainKind :: { External , Internal } ;
109+
110+ const SCHEMA_SQL : & str = "CREATE TABLE last_derivation_indices (keychain TEXT, value INTEGER);
111+ CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);
112+ CREATE TABLE checksums (keychain TEXT, checksum BLOB);
113+ CREATE INDEX idx_checksums_keychain ON checksums(keychain);" ;
114+
115+ fn setup_db ( ) -> Connection {
116+ let conn = Connection :: open_in_memory ( ) . unwrap ( ) ;
117+ conn. execute_batch ( SCHEMA_SQL ) . unwrap ( ) ;
118+ conn
119+ }
120+
121+ fn insert_keychain (
122+ conn : & Connection ,
123+ keychain : & str ,
124+ value : u32 ,
125+ checksum : & [ u8 ] ,
126+ ) -> rusqlite:: Result < ( ) > {
127+ conn. execute (
128+ "INSERT INTO last_derivation_indices (keychain, value) VALUES (?, ?)" ,
129+ rusqlite:: params![ keychain, value] ,
130+ ) ?;
131+ conn. execute (
132+ "INSERT INTO checksums (keychain, checksum) VALUES (?, ?)" ,
133+ rusqlite:: params![ keychain, checksum] ,
134+ ) ?;
135+ Ok ( ( ) )
136+ }
74137
75- #[ cfg( feature = "rusqlite" ) ]
76138 #[ test]
77139 fn test_get_pre_1_wallet_keychains ( ) -> anyhow:: Result < ( ) > {
78- let mut conn = Connection :: open_in_memory ( ) ?;
79- let external_checksum = vec ! [ 0x01u8 , 0x02u8 , 0x03u8 , 0x04u8 ] ;
80- let internal_checksum = vec ! [ 0x05u8 , 0x06u8 , 0x07u8 , 0x08u8 ] ;
81- // Init tables
82- {
83- // Create pre-1.0 bdk sqlite schema
84- conn. execute_batch (
85- "CREATE TABLE last_derivation_indices (keychain TEXT, value INTEGER);
86- CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);
87- CREATE TABLE checksums (keychain TEXT, checksum BLOB);
88- CREATE INDEX idx_checksums_keychain ON checksums(keychain);" ,
89- ) ?;
90- // Insert test data
91- conn. execute (
92- "INSERT INTO last_derivation_indices (keychain, value) VALUES (?, ?)" ,
93- rusqlite:: params![ "\" External\" " , 42 ] ,
94- ) ?;
95- conn. execute (
96- "INSERT INTO checksums (keychain, checksum) VALUES (?, ?)" ,
97- rusqlite:: params![ "\" External\" " , external_checksum] ,
98- ) ?;
99- conn. execute (
100- "INSERT INTO last_derivation_indices (keychain, value) VALUES (?, ?)" ,
101- rusqlite:: params![ "\" Internal\" " , 21 ] ,
102- ) ?;
103- conn. execute (
104- "INSERT INTO checksums (keychain, checksum) VALUES (?, ?)" ,
105- rusqlite:: params![ "\" Internal\" " , internal_checksum] ,
106- ) ?;
107- }
140+ let mut conn = setup_db ( ) ;
141+ let external_checksum = "72k0lrja" ;
142+ let internal_checksum = "07nwzkz9" ;
143+
144+ insert_keychain ( & conn, "\" External\" " , 42 , external_checksum. as_bytes ( ) ) ?;
145+ insert_keychain ( & conn, "\" Internal\" " , 21 , internal_checksum. as_bytes ( ) ) ?;
108146
109147 // test with a 2 keychain wallet
110- let result = super :: get_pre_1_wallet_keychains ( & mut conn) ?;
148+ let result = super :: get_pre_v1_wallet_keychains ( & mut conn) ?;
111149 assert_eq ! ( result. len( ) , 2 ) ;
112- assert_eq ! ( result[ 0 ] . keychain, " External" ) ;
150+ assert_eq ! ( result[ 0 ] . keychain, External ) ;
113151 assert_eq ! ( result[ 0 ] . last_derivation_index, 42 ) ;
114152 assert_eq ! ( result[ 0 ] . checksum, external_checksum) ;
115- assert_eq ! ( result[ 1 ] . keychain, " Internal" ) ;
153+ assert_eq ! ( result[ 1 ] . keychain, Internal ) ;
116154 assert_eq ! ( result[ 1 ] . last_derivation_index, 21 ) ;
117155 assert_eq ! ( result[ 1 ] . checksum, internal_checksum) ;
118156 // delete "Internal" descriptor
@@ -127,12 +165,63 @@ mod test {
127165 ) ?;
128166 }
129167 // test with a 1 keychain wallet
130- let result = super :: get_pre_1_wallet_keychains ( & mut conn) ?;
168+ let result = super :: get_pre_v1_wallet_keychains ( & mut conn) ?;
131169 assert_eq ! ( result. len( ) , 1 ) ;
132- assert_eq ! ( result[ 0 ] . keychain, " External" ) ;
170+ assert_eq ! ( result[ 0 ] . keychain, External ) ;
133171 assert_eq ! ( result[ 0 ] . last_derivation_index, 42 ) ;
134172 assert_eq ! ( result[ 0 ] . checksum, external_checksum) ;
135173
136174 Ok ( ( ) )
137175 }
176+
177+ #[ test]
178+ fn test_invalid_keychain_name ( ) {
179+ let mut conn = setup_db ( ) ;
180+ insert_keychain ( & conn, "\" InvalidKeychain\" " , 42 , b"72k0lrja" ) . unwrap ( ) ;
181+
182+ let result = super :: get_pre_v1_wallet_keychains ( & mut conn) ;
183+ assert ! ( result. is_err( ) ) ;
184+ let err = result. unwrap_err ( ) ;
185+ assert ! (
186+ matches!( err, super :: PreV1MigrationError :: InvalidKeychain ( ref name) if name == "InvalidKeychain" ) ,
187+ "Expected InvalidKeychain error with name 'InvalidKeychain', got: {:?}" ,
188+ err
189+ ) ;
190+ }
191+
192+ #[ test]
193+ fn test_invalid_checksum_utf8 ( ) {
194+ let mut conn = setup_db ( ) ;
195+ insert_keychain ( & conn, "\" External\" " , 42 , & [ 0xFF , 0xFE , 0xFD ] ) . unwrap ( ) ;
196+
197+ let result = super :: get_pre_v1_wallet_keychains ( & mut conn) ;
198+ assert ! ( result. is_err( ) ) ;
199+ let err = result. unwrap_err ( ) ;
200+ assert ! (
201+ matches!( err, super :: PreV1MigrationError :: InvalidChecksum ( _) ) ,
202+ "Expected InvalidChecksum error, got: {:?}" ,
203+ err
204+ ) ;
205+ }
206+
207+ #[ test]
208+ fn test_empty_database ( ) -> anyhow:: Result < ( ) > {
209+ let mut conn = setup_db ( ) ;
210+ let result = super :: get_pre_v1_wallet_keychains ( & mut conn) ?;
211+ assert_eq ! ( result. len( ) , 0 ) ;
212+ Ok ( ( ) )
213+ }
214+
215+ #[ test]
216+ fn test_missing_table ( ) {
217+ let mut conn = Connection :: open_in_memory ( ) . unwrap ( ) ;
218+ let result = super :: get_pre_v1_wallet_keychains ( & mut conn) ;
219+ assert ! ( result. is_err( ) ) ;
220+ let err = result. unwrap_err ( ) ;
221+ assert ! (
222+ matches!( err, super :: PreV1MigrationError :: RusqliteError ( _) ) ,
223+ "Expected RusqliteError, got: {:?}" ,
224+ err
225+ ) ;
226+ }
138227}
0 commit comments