55from binascii import hexlify
66from embit .liquid .networks import NETWORKS
77from embit import bip32
8- from helpers import is_liquid
8+ from helpers import is_liquid , SDCardFile
99from io import BytesIO
1010import platform
11- import lvgl as lv
1211from collections import OrderedDict
1312
1413class XpubApp (BaseApp ):
@@ -41,7 +40,8 @@ async def menu(self, show_screen, show_all=False):
4140 (0 , "Show more keys" ),
4241 (2 , "Change account number" ),
4342 (1 , "Enter custom derivation" ),
44- (3 , "Export all keys to SD" ),
43+ (3 , "Export all keys for this account" ),
44+ (4 , "Export multiple accounts" ),
4545 ]
4646 else :
4747 buttons = [
@@ -69,9 +69,14 @@ async def menu(self, show_screen, show_all=False):
6969 ),
7070 ]
7171 # wait for menu selection
72- menuitem = await show_screen (Menu (buttons , last = (255 , None ),
73- title = "Select the key" ,
74- note = "Current account number: %d" % self .account ))
72+ menuitem = await show_screen (
73+ Menu (
74+ buttons ,
75+ title = "Select the key" ,
76+ note = "Current account number: %d" % self .account ,
77+ last = (255 , None ),
78+ )
79+ )
7580
7681 # process the menu button:
7782 # back button
@@ -84,6 +89,15 @@ async def menu(self, show_screen, show_all=False):
8489 if der is not None :
8590 await self .show_xpub (der , show_screen )
8691 return True
92+ elif menuitem == 2 :
93+ account = await show_screen (NumericScreen (current_val = str (self .account )))
94+ if account and int (account ) >= 0x80000000 :
95+ raise AppError ("Account number too large" )
96+ try :
97+ self .account = int (account )
98+ except :
99+ self .account = 0
100+ return await self .menu (show_screen )
87101 elif menuitem == 3 :
88102 file_format = await self .save_menu (show_screen )
89103 if file_format :
@@ -93,66 +107,143 @@ async def menu(self, show_screen, show_all=False):
93107 "Public keys are saved to the file:\n \n %s" % filename ,
94108 button_text = "Close" )
95109 )
96- elif menuitem == 2 :
97- account = await show_screen (NumericScreen (current_val = str (self .account )))
98- if account and int (account ) > 0x80000000 :
99- raise AppError ('Account number too large' )
100- try :
101- self .account = int (account )
102- except :
103- self .account = 0
104- return await self .menu (show_screen )
110+ elif menuitem == 4 :
111+ from_account = await show_screen (
112+ NumericScreen (
113+ title = "Enter START account number" ,
114+ current_val = str (self .account )
115+ )
116+ )
117+ to_account = await show_screen (
118+ NumericScreen (
119+ title = "Enter END account number" ,
120+ current_val = str (self .account )
121+ )
122+ )
123+ if from_account is None or to_account is None :
124+ return
125+ if from_account == "" :
126+ from_account = self .account
127+ if to_account == "" :
128+ to_account = self .account
129+ from_account = int (from_account )
130+ to_account = int (to_account )
131+ file_format = await self .save_menu (show_screen )
132+ await self .export_multiple_accounts_xpubs (
133+ from_account , to_account , file_format , show_screen
134+ )
105135 else :
106136 await self .show_xpub (menuitem , show_screen )
107137 return True
108138 return False
109139
110- def save_all_to_sd (self , file_format ):
140+ def save_all_to_sd (self , file_format , account = None ):
141+ if account is None :
142+ account = self .account
143+
111144 fingerprint = hexlify (self .keystore .fingerprint ).decode ()
112- extension = "json"
113- coin = NETWORKS [self .network ]["bip32" ]
114145
146+ extension = "txt" if file_format == self .export_specter_diy else "json"
147+ filename = "%s-%s-%d-all.%s" % (
148+ file_format , fingerprint , account , extension ,
149+ )
150+
151+ if not platform .is_sd_present ():
152+ raise AppError ("Please insert SD card" )
153+
154+ with SDCardFile (filename , "w" ) as f :
155+ self ._dump_account (f , file_format , account )
156+
157+ return filename
158+
159+ async def export_multiple_accounts_xpubs (
160+ self ,
161+ from_account ,
162+ to_account ,
163+ file_format ,
164+ show_screen ,
165+ ):
166+ if from_account > to_account :
167+ from_account , to_account = to_account , from_account
168+ if to_account >= 0x80000000 :
169+ raise AppError ('Account number too large' )
170+ fingerprint = hexlify (self .keystore .fingerprint ).decode ()
171+ if file_format == self .export_specter_diy :
172+ # in our format we can dump any number of accounts in one file
173+ filename = "%s-%s-%d-%d.txt" % (
174+ file_format , fingerprint , from_account , to_account
175+ )
176+ with SDCardFile (filename , "w" ) as f :
177+ for account in range (from_account , to_account + 1 ):
178+ self .show_loader (title = "Exporting account %d..." % account )
179+ self ._dump_account (f , file_format , account )
180+ await show_screen (
181+ Alert (
182+ "Success!" ,
183+ "File was successfully saved under:\n \n %s" % filename ,
184+ button_text = "OK" ,
185+ )
186+ )
187+ else : # cc format - one file per account
188+ for account in range (from_account , to_account + 1 ):
189+ self .show_loader (title = "Exporting account %d..." % account )
190+ self .save_all_to_sd (file_format , account )
191+ await show_screen (
192+ Alert (
193+ "Success!" ,
194+ "All accounts are saved to corresponding files." ,
195+ button_text = "OK" ,
196+ )
197+ )
198+
199+ def _dump_account (self , f , file_format , account ):
200+ """dump all keys of one account to a file"""
201+ coin = NETWORKS [self .network ]["bip32" ]
115202 derivations = [
116- ('bip84' , "p2wpkh" , "m/84'/%d'/%d'" % (coin , self . account )),
117- ('bip86' , "p2tr" , "m/86'/%d'/%d'" % (coin , self . account )),
118- ('bip49' , "p2sh-p2wpkh" , "m/49'/%d'/%d'" % (coin , self . account )),
119- ('bip44' , "p2pkh" , "m/44'/%d'/%d'" % (coin , self . account )),
120- ('bip48_1' , "p2sh-p2wsh" , "m/48'/%d'/%d'/1'" % (coin , self . account )),
121- ('bip48_2' , "p2wsh" , "m/48'/%d'/%d'/2'" % (coin , self . account )),
203+ ('bip84' , "p2wpkh" , "m/84'/%d'/%d'" % (coin , account )),
204+ ('bip86' , "p2tr" , "m/86'/%d'/%d'" % (coin , account )),
205+ ('bip49' , "p2sh-p2wpkh" , "m/49'/%d'/%d'" % (coin , account )),
206+ ('bip44' , "p2pkh" , "m/44'/%d'/%d'" % (coin , account )),
207+ ('bip48_1' , "p2sh-p2wsh" , "m/48'/%d'/%d'/1'" % (coin , account )),
208+ ('bip48_2' , "p2wsh" , "m/48'/%d'/%d'/2'" % (coin , account )),
122209 ]
210+ fingerprint = hexlify (self .keystore .fingerprint ).decode ()
123211
124212 if file_format == self .export_specter_diy :
125- # text file with [fgp/der]xpub lines
126- filedata = ""
127- for der in derivations :
128- xpub = self .keystore .get_xpub (der [2 ])
129- filedata += "[%s/%s]%s\n " % (fingerprint , der [2 ][2 :].replace ("'" ,"h" ), xpub .to_base58 (NETWORKS [self .network ]["xpub" ]))
130- extension = "txt"
213+ for keytype , scripttype , der in derivations :
214+ xpub = self .keystore .get_xpub (der )
215+ f .write ("[%s/%s]%s\n " % (
216+ fingerprint ,
217+ der .replace ("m/" , "" ).replace ("'" ,"h" ),
218+ xpub .to_base58 (NETWORKS [self .network ]["xpub" ]),
219+ ))
131220 else :
132221 # coldcard generic json format
133222 m = self .keystore .get_xpub ("m" )
134223 data = {
135224 "xpub" : m .to_base58 (NETWORKS [self .network ]["xpub" ]),
136225 "xfp" : fingerprint ,
137- "account" : self . account ,
226+ "account" : account ,
138227 "chain" : "BTC" if self .network == "main" else "XTN"
139228 }
140229
141- for der in derivations :
142- xpub = self .keystore .get_xpub (der [ 2 ] )
230+ for keytype , scripttype , der in derivations :
231+ xpub = self .keystore .get_xpub (der )
143232
144- data [der [ 0 ] ] = {
145- "name" : der [ 1 ] ,
146- "deriv" : der [ 2 ] ,
233+ data [keytype ] = {
234+ "name" : scripttype ,
235+ "deriv" : der ,
147236 "xpub" : xpub .to_base58 (NETWORKS [self .network ]["xpub" ]),
148- "_pub" : xpub .to_base58 (bip32 .detect_version (der [2 ], default = "xpub" , network = NETWORKS [self .network ]))
237+ "_pub" : xpub .to_base58 (
238+ bip32 .detect_version (
239+ der ,
240+ default = "xpub" ,
241+ network = NETWORKS [self .network ]
242+ )
243+ )
149244 }
150245
151- filedata = json .dumps (data ).encode ()
152-
153- filename = "%s-%s-%d-all.%s" % (file_format , fingerprint , self .account , extension )
154- self .write_file (filename , filedata )
155- return filename
246+ json .dump (data , f )
156247
157248 async def process_host_command (self , stream , show_screen ):
158249 if self .keystore .is_locked :
@@ -192,12 +283,15 @@ async def show_xpub(self, derivation, show_screen):
192283 fingerprint ,
193284 derivation [1 :],
194285 )
195- res = await show_screen (XPubScreen (xpub = canonical , slip132 = slip132 , prefix = prefix ))
286+ res = await show_screen (
287+ XPubScreen (xpub = canonical , slip132 = slip132 , prefix = prefix )
288+ )
196289 if res == XPubScreen .CREATE_WALLET :
197290 await self .create_wallet (derivation , canonical , prefix , ver , show_screen )
198291 elif res :
199292 filename = "%s-%s.txt" % (fingerprint , derivation [2 :].replace ("/" , "-" ))
200- self .write_file (filename , res )
293+ with SDCardFile (filename , "w" ) as f :
294+ f .write (res )
201295 await show_screen (
202296 Alert ("Saved!" ,
203297 "Extended public key is saved to the file:\n \n %s" % filename ,
@@ -283,14 +377,6 @@ async def create_wallet(self, derivation, xpub, prefix, version, show_screen):
283377 await self .communicate (stream , app = "wallets" )
284378
285379
286- def write_file (self , filename , filedata ):
287- if not platform .is_sd_present ():
288- raise AppError ("SD card is not present" )
289- platform .mount_sdcard ()
290- with open (platform .fpath ("/sd/%s" % filename ), "w" ) as f :
291- f .write (filedata )
292- platform .unmount_sdcard ()
293-
294380 async def save_menu (self , show_screen ):
295381 buttons = [(0 , "Specter-DIY (plaintext)" ), (1 , "Cold Card (json)" )]
296382 # wait for menu selection
@@ -307,6 +393,7 @@ async def save_menu(self, show_screen):
307393 return self .export_coldcard
308394 return None
309395
396+
310397 def wipe (self ):
311398 # nothing to delete
312399 pass
0 commit comments