diff --git a/README.md b/README.md index 4730337e..ed89842a 100644 --- a/README.md +++ b/README.md @@ -56,22 +56,19 @@ https://youtu.be/aIIc2DiZYcI * Only valid input letters appear during seed word entry (time-saver!) ### Considerations: -* Built for compatibility with Specter v1.1.0 and up (support for other wallet coordinators coming) +* Built for compatibility with Specter-desktop, Sparrow and BlueWallet Vaults * Current release takes ~50 seconds to boot before menu appears (be patient!) -* Use Specter's "other" device type when adding as a new signing device -* ALWAYS opt to use animated QR codes in Specter-desktop * Always test your setup before transfering larger amounts of bitcoin (try testnet first!) * Currently ONLY generating Native Segwit Multisig Xpubs -* Scanning animated QRs into a PC is tricky, be aware of ambient light, glare and focus * (Holding the screen upside-down or at a slight angle can significantly reduce glare) -* Display text is small; comes with 240x240 pixel territory but ever trying to optimize -* Check out our "seedsigner" telegram community for community help / feedback: (https://t.me/joinchat/GHNuc_nhNQjLPWsS) +* Check out our "seedsigner" telegram group for community help / feedback: (https://t.me/joinchat/GHNuc_nhNQjLPWsS) * If you think SeedSigner adds value to the Bitcoin ecosystem, please help me spread the word! (tweets, pics, videos, etc.) ### Planned Upcoming Improvements / Functionality: -* Support for Sparrow & Bluewallet multisig coordinators! * Support for single-signature XPUB generation / signing -* Manually step through QR sequences using button/joystick controls +* Support for BIP39 passphrases +* User-guided manual QR transcription to quickly input seed phrases +* Low/Med/High customizable QR density * Other optimizations based on user feedback! --------------- diff --git a/requirements.txt b/requirements.txt index 43487108..27f64fed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -embit==0.4.2 +embit==0.4.3 picamera==1.13 Pillow==8.2.0 pyzbar==0.1.8 qrcode==6.1 RPi.GPIO==0.7.0 six==1.16.0 -spidev==3.5 \ No newline at end of file +spidev==3.5 diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 421dcfae..0be791d9 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -554,11 +554,19 @@ class Controller: if raw_pbst == "nodata": return Path.MAIN_MENU if raw_pbst == "invalid": - self.menu_view.draw_modal(["QR Format Unexpected", "Check Wallet in Settings"], "", "RIGHT to EXIT") + self.menu_view.draw_modal(["QR Format Unexpected", "Check Wallet in Settings"], "", "Right to Exit") + input = self.buttons.wait_for([B.KEY_RIGHT]) + return Path.MAIN_MENU + if raw_pbst == "invalidpsbt": + self.menu_view.draw_modal(["PSBT UR 2.0 Decoding Error", "try again"], "", "Right to Exit") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU self.menu_view.draw_modal(["Parsing PSBT ..."]) - self.wallet.parse_psbt(raw_pbst) + parse_status = self.wallet.parse_psbt(raw_pbst) + if parse_status == False: + self.menu_view.draw_modal(["PSBT Parsing Failed"], "", "Right to Exit") + input = self.buttons.wait_for([B.KEY_RIGHT]) + return Path.MAIN_MENU # show transaction information before sign self.signing_tools_view.display_transaction_information(self.wallet) diff --git a/src/seedsigner/models/blue_wallet.py b/src/seedsigner/models/blue_wallet.py index e7a6bbce..14d1ffa5 100644 --- a/src/seedsigner/models/blue_wallet.py +++ b/src/seedsigner/models/blue_wallet.py @@ -124,7 +124,7 @@ class BlueWallet(Wallet): def set_qr_density(self, density): self.cur_qr_density = density if density == Wallet.QRLOW: - self.qrsize = 70 + self.qrsize = 20 elif density == Wallet.QRMEDIUM: self.qrsize = 100 elif density == Wallet.QRHIGH: diff --git a/src/seedsigner/models/generic_ur2_wallet.py b/src/seedsigner/models/generic_ur2_wallet.py index a7c9ad07..9cdd87df 100644 --- a/src/seedsigner/models/generic_ur2_wallet.py +++ b/src/seedsigner/models/generic_ur2_wallet.py @@ -102,9 +102,9 @@ class GenericUR2Wallet(Wallet): if not self.capture_complete(): View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT25) - View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill="ORANGE", font=View.IMPACT25) + View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.IMPACT25) tw, th = View.draw.textsize(str(round(self.percentage_complete * 100)) + "% Complete", font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), str(round(self.percentage_complete * 100)) + "% Complete", fill="ORANGE", font=View.IMPACT22) + View.draw.text(((240 - tw) / 2, 125), str(round(self.percentage_complete * 100)) + "% Complete", fill=View.color, font=View.IMPACT22) View.DispShowImage() elif self.scan_started_ind == 0: diff --git a/src/seedsigner/models/sparrow_wallet.py b/src/seedsigner/models/sparrow_wallet.py index 12f8caa9..6e9c77a9 100644 --- a/src/seedsigner/models/sparrow_wallet.py +++ b/src/seedsigner/models/sparrow_wallet.py @@ -34,10 +34,12 @@ class SparrowWallet(Wallet): return "Sparrow" def parse_psbt(self, raw_psbt) -> bool: - base64_psbt = a2b_base64(raw_psbt) - self.tx = psbt.PSBT.parse(base64_psbt) - - self._parse_psbt() + try: + base64_psbt = a2b_base64(raw_psbt) + self.tx = psbt.PSBT.parse(base64_psbt) + self._parse_psbt() + except Exception: + return False return True @@ -103,7 +105,11 @@ class SparrowWallet(Wallet): self.qr_data = ["empty"] # get data and percentage - self.ur_decoder.receive_part(data[0]) + decoder_work_check = self.ur_decoder.receive_part(data[0]) + if decoder_work_check == False: + self.qr_data = ["invalidpsbt"] + self.buttons.trigger_override() # something went wrong, invalid QR + return self.percentage_complete = self.ur_decoder.estimated_percent_complete() # checking if all frames has been captured, exit camera processing @@ -127,7 +133,9 @@ class SparrowWallet(Wallet): def total_frames_parse(data) -> int: if re.search("^UR\:CRYPTO-PSBT\/(\d+)\-(\d+)\/", data, re.IGNORECASE) != None: - return 10 #valid + return 10 # valid + elif re.search("^UR\:CRYPTO-PSBT", data, re.IGNORECASE) != None: + return 1 # valid but only 1 segment else: return -1 #invalid diff --git a/src/seedsigner/models/specter_desktop_wallet.py b/src/seedsigner/models/specter_desktop_wallet.py index a6cbe58b..dff29997 100644 --- a/src/seedsigner/models/specter_desktop_wallet.py +++ b/src/seedsigner/models/specter_desktop_wallet.py @@ -21,11 +21,12 @@ class SpecterDesktopWallet(Wallet): return "Specter Desktop" def parse_psbt(self, raw_psbt) -> bool: - - base64_psbt = a2b_base64(raw_psbt) - self.tx = psbt.PSBT.parse(base64_psbt) - - self._parse_psbt() + try: + base64_psbt = a2b_base64(raw_psbt) + self.tx = psbt.PSBT.parse(base64_psbt) + self._parse_psbt() + except Exception: + return False return True @@ -54,14 +55,18 @@ class SpecterDesktopWallet(Wallet): def total_frames_parse(data) -> int: if re.search("^p(\d+)of(\d+) ", data, re.IGNORECASE) != None: return int(re.search("^p(\d+)of(\d+) ", data, re.IGNORECASE).group(2)) + elif re.search("UR\:.*", data, re.IGNORECASE) != None: + return -1 # if data has UR:, assume invalid wallet selected else: - return -1 + return 1 def current_frame_parse(data) -> int: if re.search("^p(\d+)of(\d+) ", data, re.IGNORECASE) != None: return int(re.search("^p(\d+)of(\d+) ", data, re.IGNORECASE).group(1)) + elif re.search("UR\:.*", data, re.IGNORECASE) != None: + return -1 # if data has UR:, assume invalid wallet selected else: - return -1 + return 1 # assume a single frame for specter if no p#of# is found. def data_parse(data) -> str: return data.split(" ")[-1].strip() @@ -96,7 +101,12 @@ class SpecterDesktopWallet(Wallet): stop = self.qrsize qr_cnt = ((len(data)-1) // self.qrsize) + 1 - while cnt < qr_cnt: + if qr_cnt == 1: + part = data[start:stop] + images.append(qr.qrimage(part)) + print(part) + + while cnt < qr_cnt and qr_cnt != 1: part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + data[start:stop] images.append(qr.qrimage(part)) print(part) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 8058a83d..9471912a 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -805,33 +805,33 @@ class SeedToolsView(View): # Screen Title View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) tw, th = View.draw.textsize("Add Passphrase", font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 2), "Add Passphrase", fill="ORANGE", font=View.IMPACT18) + View.draw.text(((240 - tw) / 2, 2), "Add Passphrase", fill=View.color, font=View.IMPACT18) # Screen Botton tw, th = View.draw.textsize("use joystick to add and", font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 197), "use joystick to add and", fill="ORANGE", font=View.IMPACT18) + View.draw.text(((240 - tw) / 2, 197), "use joystick to add and", fill=View.color, font=View.IMPACT18) tw, th = View.draw.textsize("remove passphrase characters", font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 217), "remove passphrase characters", fill="ORANGE", font=View.IMPACT18) + View.draw.text(((240 - tw) / 2, 217), "remove passphrase characters", fill=View.color, font=View.IMPACT18) # Display passphrase selection in progress - View.draw.text((5,33), "Phrase:", fill="ORANGE", font=View.IMPACT20) - View.draw.text((72,35), self.passphrase[:14] + "▒", fill="ORANGE", font=View.COURIERNEW20) + View.draw.text((5,33), "Phrase:", fill=View.color, font=View.IMPACT20) + View.draw.text((72,35), self.passphrase[:14] + "▒", fill=View.color, font=View.COURIERNEW20) if len(self.passphrase) >= 14: - View.draw.text((72,50), self.passphrase[14:] + "▒", fill="ORANGE", font=View.COURIERNEW20) + View.draw.text((72,50), self.passphrase[14:] + "▒", fill=View.color, font=View.COURIERNEW20) # Display message when max passphrase length reached if len(self.passphrase) >= 28: - View.draw.text((50,70), "Max passphrase of 28", fill="ORANGE", font=View.IMPACT18) - View.draw.text((53,90), "characters reached", fill="ORANGE", font=View.IMPACT18) + View.draw.text((50,70), "Max passphrase of 28", fill=View.color, font=View.IMPACT18) + View.draw.text((53,90), "characters reached", fill=View.color, font=View.IMPACT18) # Save Button c_x_offset = 240 - View.IMPACT25.getsize("Exit")[0] - View.draw.text((c_x_offset , 172), "Exit", fill="ORANGE", font=View.IMPACT22) + View.draw.text((c_x_offset , 172), "Exit", fill=View.color, font=View.IMPACT22) # Toglle Button x = 240 - View.IMPACT20.getsize(self.pass_case_toggle)[0] - View.draw.text((x, 110), self.pass_case_toggle, fill="ORANGE", font=View.IMPACT20) + View.draw.text((x, 110), self.pass_case_toggle, fill=View.color, font=View.IMPACT20) # draw letter and arrows if self.pass_case_toggle == "lower": @@ -842,17 +842,15 @@ class SeedToolsView(View): pass_letter_disp = self.pass_letter tw, th = View.draw.textsize(pass_letter_disp, font=View.IMPACT35) - View.draw.text((((30-tw)/2), 112), pass_letter_disp, fill="ORANGE", font=View.IMPACT35) - View.draw.polygon([(8, 105) , (14, 89) , (20, 105 )], outline="ORANGE", fill="BLACK") - View.draw.polygon([(8, 168), (14, 184), (20, 168)], outline="ORANGE", fill="BLACK") + View.draw.text((((30-tw)/2), 112), pass_letter_disp, fill=View.color, font=View.IMPACT35) + View.draw.polygon([(8, 105) , (14, 89) , (20, 105 )], outline=View.color, fill="BLACK") + View.draw.polygon([(8, 168), (14, 184), (20, 168)], outline=View.color, fill="BLACK") View.DispShowImage() return def gather_passphrase_up(self): - # View.draw.polygon([(8, 105) , (14, 89) , (20, 105 )], outline="ORANGE", fill="ORANGE") - # View.DispShowImage() pass_choice = self.get_pass_value_options() @@ -863,8 +861,6 @@ class SeedToolsView(View): self.pass_letter = pass_choice[idx-1] def gather_passphrase_down(self): - # View.draw.polygon([(8, 168) , (14, 184) , (20, 168 )], outline="ORANGE", fill="ORANGE") - # View.DispShowImage() pass_choice = self.get_pass_value_options() @@ -925,7 +921,10 @@ class SeedToolsView(View): self.draw.text(((240 - tw) / 2, 60), "The final word is :", fill=View.color, font=View.IMPACT23) tw, th = self.draw.textsize(last_word, font=View.IMPACT50) self.draw.text(((240 - tw) / 2, 90), last_word, fill=View.color, font=View.IMPACT50) - self.draw.text((73, 210), "Right to Continue", fill=View.color, font=View.IMPACT18) + + tw, th = View.draw.textsize("Right to Continue", font=View.IMPACT18) + View.draw.text(((240 - tw) / 2, 210), "Right to Continue", fill=View.color, font=View.IMPACT18) + View.DispShowImage() input = self.buttons.wait_for([B.KEY_RIGHT]) @@ -1307,14 +1306,14 @@ class SeedToolsView(View): draw.rectangle((View.canvas_width - mask_width - 1, mask_height, View.canvas_width, View.canvas_height - mask_height), fill=mask_rgba) # Draw a box around the cutout portion of the mask for better visibility - draw.line((mask_width, mask_height, mask_width, View.canvas_height - mask_height), fill="ORANGE") - draw.line((View.canvas_width - mask_width, mask_height, View.canvas_width - mask_width, View.canvas_height - mask_height), fill="ORANGE") - draw.line((mask_width, mask_height, View.canvas_width - mask_width, mask_height), fill="ORANGE") - draw.line((mask_width, View.canvas_height - mask_height, View.canvas_width - mask_width, View.canvas_height - mask_height), fill="ORANGE") + draw.line((mask_width, mask_height, mask_width, View.canvas_height - mask_height), fill=View.color) + draw.line((View.canvas_width - mask_width, mask_height, View.canvas_width - mask_width, View.canvas_height - mask_height), fill=View.color) + draw.line((mask_width, mask_height, View.canvas_width - mask_width, mask_height), fill=View.color) + draw.line((mask_width, View.canvas_height - mask_height, View.canvas_width - mask_width, View.canvas_height - mask_height), fill=View.color) msg = "click to exit" tw, th = draw.textsize(msg, font=View.IMPACT18) - draw.text(((View.canvas_width - tw) / 2, View.canvas_height - th - 2), msg, fill="ORANGE", font=View.IMPACT18) + draw.text(((View.canvas_width - tw) / 2, View.canvas_height - th - 2), msg, fill=View.color, font=View.IMPACT18) def draw_block_labels(cur_block_x, cur_block_y): # Create overlay for block labels (e.g. "D-5") @@ -1323,8 +1322,8 @@ class SeedToolsView(View): block_labels = Image.new("RGBA", (View.canvas_width, View.canvas_height), (255,255,255,0)) draw = ImageDraw.Draw(block_labels) - draw.rectangle((mask_width, 0, View.canvas_width - mask_width, pixels_per_block), fill="ORANGE") - draw.rectangle((0, mask_height, pixels_per_block, View.canvas_height - mask_height), fill="ORANGE") + draw.rectangle((mask_width, 0, View.canvas_width - mask_width, pixels_per_block), fill=View.color) + draw.rectangle((0, mask_height, pixels_per_block, View.canvas_height - mask_height), fill=View.color) label_font = View.COURIERNEW24 x_label = block_labels_x[cur_block_x] diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index 5cbbc923..9aa9e007 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -96,22 +96,13 @@ class SettingsToolsView(View): line1 = "SeedSigner" line2 = "Version v" + self.controller.VERSION - line3 = "built for use with" - line4 = "Specter-desktop" - line5 = "v1.1.0 or higher" - line6 = "(Joystick RIGHT to EXIT)" + line3 = "(Joystick RIGHT to EXIT)" View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) tw, th = View.draw.textsize(line1, font=View.IMPACT22) View.draw.text(((240 - tw) / 2, 20), line1, fill=View.color, font=View.IMPACT22) tw, th = View.draw.textsize(line2, font=View.IMPACT22) View.draw.text(((240 - tw) / 2, 55), line2, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line3, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), line3, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line4, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), line4, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line5, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 160), line5, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line6, font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), line6, fill=View.color, font=View.IMPACT18) - View.DispShowImage() \ No newline at end of file + tw, th = View.draw.textsize(line3, font=View.IMPACT18) + View.draw.text(((240 - tw) / 2, 210), line3, fill=View.color, font=View.IMPACT18) + View.DispShowImage() diff --git a/src/seedsigner/views/signing_tools_view.py b/src/seedsigner/views/signing_tools_view.py index bf38cca1..08eda71e 100644 --- a/src/seedsigner/views/signing_tools_view.py +++ b/src/seedsigner/views/signing_tools_view.py @@ -109,14 +109,14 @@ class SigningToolsView(View): self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) tw, th = self.draw.textsize("Confirm Tx Details", font=View.IMPACT25) - self.draw.text(((240 - tw) / 2, 3), "Confirm Tx Details", fill="ORANGE", font=View.IMPACT25) + self.draw.text(((240 - tw) / 2, 3), "Confirm Tx Details", fill=View.color, font=View.IMPACT25) in_fee_outs_str = str(wallet.ins) in_fee_outs_str += " inputs - fee = " if wallet.ins > 1 else " input - fee = " in_fee_outs_str += str(wallet.outs) in_fee_outs_str += " outs" if wallet.outs > 1 else " out" tw, th = self.draw.textsize(in_fee_outs_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill="ORANGE", font=View.IMPACT22) + self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill=View.color, font=View.IMPACT22) receiving_addr_str1 = "" receiving_addr_str2 = "" @@ -130,32 +130,32 @@ class SigningToolsView(View): receiving_addr_str1 += "Self-Transfer" tw, th = self.draw.textsize(receiving_addr_str1, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill="ORANGE", font=View.IMPACT22) + self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill=View.color, font=View.IMPACT22) if len(receiving_addr_str2) > 0: tw, th = self.draw.textsize(receiving_addr_str2, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 100), receiving_addr_str2, fill="ORANGE", font=View.IMPACT22) + self.draw.text(((240 - tw) / 2, 100), receiving_addr_str2, fill=View.color, font=View.IMPACT22) if wallet.spend > 0: spending_str = "Spend: " + str(wallet.spend) + " sats" tw, th = self.draw.textsize(spending_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 130), spending_str, fill="ORANGE", font=View.IMPACT22) + self.draw.text(((240 - tw) / 2, 130), spending_str, fill=View.color, font=View.IMPACT22) if wallet.change > 0 and wallet.dest_addr_cnt == 0: change_str = "Amount: " + str(int(wallet.change)) + " sats" tw, th = self.draw.textsize(change_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 155), change_str, fill="ORANGE", font=View.IMPACT22) + self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22) elif wallet.change > 0: change_str = "Change: " + str(int(wallet.change)) + " sats" tw, th = self.draw.textsize(change_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 155), change_str, fill="ORANGE", font=View.IMPACT22) + self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22) fee_str = "Fee: " + str(int(wallet.fee)) + " sats" tw, th = self.draw.textsize(fee_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 180), fee_str, fill="ORANGE", font=View.IMPACT22) + self.draw.text(((240 - tw) / 2, 180), fee_str, fill=View.color, font=View.IMPACT22) tw, th = self.draw.textsize("Left to Exit, Right to Continue", font=View.IMPACT18) - self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill="ORANGE", font=View.IMPACT18) + self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill=View.color, font=View.IMPACT18) View.DispShowImage()