diff --git a/README.md b/README.md index bb933e6..39e0050 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ python multiwallet_gui/app.py ## Roadmap: * Allow user to select limit/offset for receive address verfication -* Add tooltips/explainers * Mainnet/testnet toggle * Add QR code generation on send/receive * Support arbitrary paths @@ -43,7 +42,7 @@ python multiwallet_gui/app.py * Better form handling/validation * Add libsec * Add webcam on receive/send -* Sign release +* Sign binaries * Dark mode * Reproducible build diff --git a/multiwallet_gui/app.py b/multiwallet_gui/app.py index 8a36829..037b37c 100644 --- a/multiwallet_gui/app.py +++ b/multiwallet_gui/app.py @@ -1,12 +1,15 @@ +#! /usr/bin/env bash + +import sys + from PyQt5.QtWidgets import ( QApplication, QDialog, + QDialogButtonBox, QTabWidget, QVBoxLayout, ) -import sys - from multiwallet_gui.seedpicker import SeedpickerTab from multiwallet_gui.receive import ReceiveTab from multiwallet_gui.send import SendTab @@ -18,25 +21,43 @@ def __init__(self): self.setWindowTitle( "Multiwallet - Stateless PSBT Multisig Wallet - ALPHA VERSION TESTNET ONLY" ) + self.setFixedWidth(800) + + self.layout = QVBoxLayout() + + self.tab_widget = QTabWidget() - vbox = QVBoxLayout() - tabWidget = QTabWidget() + if False: + # TODO: use something like this for a testnet toggle + self.buttonbox = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel + ) + self.buttonbox.accepted.connect(self.accept) + self.buttonbox.rejected.connect(self.reject) + self.layout.addWidget(self.buttonbox) - for tab in (SeedpickerTab, ReceiveTab, SendTab): - tab_obj = tab() - tabWidget.addTab(tab_obj, tab_obj.TITLE) + # Initialize tab screen + self.seedpicker_tab = SeedpickerTab() + self.receive_tab = ReceiveTab() + self.send_tab = SendTab() - vbox.addWidget(tabWidget) + # Add tabs + for cnt, tab in enumerate( + [self.seedpicker_tab, self.receive_tab, self.send_tab] + ): + self.tab_widget.addTab(tab, tab.TITLE) + self.tab_widget.setTabToolTip(cnt, tab.HOVER) - self.setLayout(vbox) - self.setFixedWidth(600) + # Add tabs to widget + self.layout.addWidget(self.tab_widget) + self.setLayout(self.layout) def main(): - app = QApplication(sys.argv) + qapp = QApplication(sys.argv) my_app = MultiwalletApp() my_app.show() - app.exec() + qapp.exec() if __name__ == "__main__": diff --git a/multiwallet_gui/receive.py b/multiwallet_gui/receive.py index 4a7ef88..d4c217b 100644 --- a/multiwallet_gui/receive.py +++ b/multiwallet_gui/receive.py @@ -104,19 +104,28 @@ def get_addresses(pubkey_dicts, quorum_m, quorum_n, limit, offset, is_testnet): class ReceiveTab(QWidget): TITLE = "Receive" + HOVER = "Verify your bitcoin addresses belong to you qourum." def __init__(self): super().__init__() vbox = QVBoxLayout() self.descriptorLabel = QLabel("Wallet Descriptor") + self.descriptorLabel.setToolTip( + "This extended public key information is used to generate your bitcoin addresses." + ) self.descriptorEdit = QPlainTextEdit("") - self.descriptorEdit.setPlaceholderText("wsh(sortedmulti(2,...") + self.descriptorEdit.setPlaceholderText( + "Something like this:\n\nwsh(sortedmulti(2,[deadbeef/48h/1h/0h/2h]xpub.../0/*," + ) self.descriptorSubmitButton = QPushButton("Derive Addresses") self.descriptorSubmitButton.clicked.connect(self.process_submit) self.addrResultsLabel = QLabel("") + self.addrResultsLabel.setToolTip( + "These bitcoin addresses belong to the quorum of extended public keys above. You may want to print this out for future reference." + ) self.addrResultsEdit = QPlainTextEdit("") self.addrResultsEdit.setReadOnly(True) self.addrResultsEdit.setHidden(True) @@ -156,7 +165,7 @@ def process_submit(self): results_label = f"{pubkeys_info['quorum_m']}-of-{pubkeys_info['quorum_n']} Multisig Addresses" if not _is_libsec_enabled(): - results_label += "\n(this is ~100x faster with libsec installed)" + results_label += "
(this is ~100x faster with libsec installed)" self.addrResultsLabel.setText(results_label) self.addrResultsEdit.setHidden(False) diff --git a/multiwallet_gui/seedpicker.py b/multiwallet_gui/seedpicker.py index 785b98e..a978ab0 100644 --- a/multiwallet_gui/seedpicker.py +++ b/multiwallet_gui/seedpicker.py @@ -1,12 +1,13 @@ #! /usr/bin/env bash from multiwallet_gui.helper import _clean_submisission, _msgbox_err + from PyQt5.QtWidgets import ( QVBoxLayout, - QWidget, QLabel, QPlainTextEdit, QPushButton, + QWidget, ) from buidl.hd import HDPrivateKey @@ -33,39 +34,57 @@ def _get_all_valid_checksum_words(first_words, first_match=True): class SeedpickerTab(QWidget): TITLE = "Seedpicker" + HOVER = ( + "Protect yourself against a bad random number generator. " + "Pick 23 words of your seed phrase and Seedpicker will calculate the last word." + ) def __init__(self): super().__init__() - vbox = QVBoxLayout() + self.layout = QVBoxLayout() self.firstWordsLabel = QLabel("First 23 Words of Your Seed") + self.firstWordsLabel.setToolTip( + "Pull words out of a hat so you don't have to trust a random number generator." + ) self.firstWordsEdit = QPlainTextEdit("") self.firstWordsEdit.setPlaceholderText( "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo" ) + # self.firstWordsSubmitButton = QPushButton(self) self.firstWordsSubmitButton = QPushButton("Calculate Full Seed") + self.firstWordsSubmitButton.setText("Calculate Full Seed") self.firstWordsSubmitButton.clicked.connect(self.process_submit) self.privResultsLabel = QLabel("") + self.privResultsLabel.setToolTip( + "Write the full mnemonic offline and store in a secure place. This represents your bitcoin private keys." + ) self.privResultsEdit = QPlainTextEdit("") self.privResultsEdit.setReadOnly(True) self.privResultsEdit.setHidden(True) self.pubResultsLabel = QLabel("") + self.pubResultsLabel.setToolTip( + "For export to your online computer and eventaully other hardware wallets. This represents your bitcoin public keys, which are neccesary-but-not-sufficient to spend your bitcoin." + ) self.pubResultsEdit = QPlainTextEdit("") self.pubResultsEdit.setReadOnly(True) self.pubResultsEdit.setHidden(True) - vbox.addWidget(self.firstWordsLabel) - vbox.addWidget(self.firstWordsEdit) - vbox.addWidget(self.firstWordsSubmitButton) - vbox.addWidget(self.privResultsLabel) - vbox.addWidget(self.privResultsEdit) - vbox.addWidget(self.pubResultsLabel) - vbox.addWidget(self.pubResultsEdit) + self.layout.addWidget(self.firstWordsLabel) + self.layout.addWidget(self.firstWordsEdit) + self.layout.addWidget(self.firstWordsSubmitButton) + self.layout.addWidget(self.privResultsLabel) + self.layout.addWidget(self.privResultsEdit) + self.layout.addWidget(self.pubResultsLabel) + self.layout.addWidget(self.pubResultsEdit) + + self.setLayout(self.layout) - self.setLayout(vbox) + # show all the widgets # TODO: needed? + self.show() def process_submit(self): # Clear any previous submission in case of errors diff --git a/multiwallet_gui/send.py b/multiwallet_gui/send.py index e4c8eca..f343608 100644 --- a/multiwallet_gui/send.py +++ b/multiwallet_gui/send.py @@ -33,6 +33,7 @@ def _format_satoshis(sats, in_btc=False): class SendTab(QWidget): TITLE = "Send" + HOVER = "Use your seed to cosign a transaction." # FIXME (add support and UX for this) UNITS = "sats" @@ -45,16 +46,26 @@ def __init__(self): self.psbtLabel = QLabel( "Partially Signed Bitcoin Transaction (required)" ) + self.psbtLabel.setToolTip( + "Transaction that your online computer is asking you to sign, in base64 format." + ) self.psbtEdit = QPlainTextEdit("") - self.psbtEdit.setPlaceholderText("cHNidP8BAH0CAAAAA...") + self.psbtEdit.setPlaceholderText("Something like this:\n\ncHNidP8BAH0CAAAAA...") - self.fullSeedLabel = QLabel("Full 24-Word Seed Phrase") + self.fullSeedLabel = QLabel("Full 24-Word Seed Phrase (optional)") + self.fullSeedLabel.setToolTip( + "Needed to sign the PSBT. You can first decode the transaction and inspect it without supplying your seed phrase." + ) self.fullSeedEdit = QPlainTextEdit("") self.fullSeedEdit.setPlaceholderText( "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo" ) self.psbtDecodedLabel = QLabel("") + self.psbtDecodedLabel.setToolTip( + "The summary of what this transaction does. Multiwallet statelessly verifies all inputs belong to the same quorum and that any change is properly returned." + ) + self.psbtDecodedEdit = QPlainTextEdit("") self.psbtDecodedEdit.setReadOnly(True) self.psbtDecodedEdit.setHidden(True) @@ -66,6 +77,9 @@ def __init__(self): self.fullSeedSubmitButton.clicked.connect(self.sign_psbt) self.psbtSignedLabel = QLabel("") + self.psbtSignedLabel.setToolTip( + "Signed version for your online computer to broadcast to the bitcoin network (once you have collected enough signatures)." + ) self.psbtSignedEdit = QPlainTextEdit("") self.psbtSignedEdit.setReadOnly(True) self.psbtSignedEdit.setHidden(True) @@ -268,6 +282,7 @@ def process_psbt(self, sign_tx=True): TX_SUMMARY = " ".join( [ + inputs_desc[0]["quorum"], "PSBT sends", _format_satoshis(output_spend_sats, in_btc=self.UNITS == "btc"), "to", @@ -307,7 +322,7 @@ def process_psbt(self, sign_tx=True): if not seed_phrase: return _msgbox_err( main_text="No Seed Phrase Supplied", - informative_text="Cannot Sign Transaction Without Seed Phrase", + informative_text="Cannot sign transaction without seed phrase", ) seed_phrase_num = len(seed_phrase.split()) diff --git a/setup.py b/setup.py index 3ca70a6..30dfb66 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="multiwallet", - version="0.3.2", + version="0.3.3", author="Michael Flaxman", author_email="multiwallet@michaelflaxman.com", description="Stateless multisig bitcoin wallet",