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",