Skip to content

Commit

Permalink
add support for mainnet and general cleanup (#4)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Flaxman <[email protected]>
  • Loading branch information
mflaxman and Michael Flaxman authored Nov 17, 2020
1 parent 70c6efc commit 3b795c8
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 50 deletions.
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,49 @@ python multiwallet_gui/app.py
```

## Roadmap:
* Mainnet/testnet toggle on sending
* Add detailed TX view (not just summary) to UI
* Add QR code generation on send/receive
* Support arbitrary paths
* Add units (sats/BTC) toggle
* Test/release on multiple OS
* Better form handling/validation
* Support arbitrary paths
* Add libsec
* Add webcam on receive/send
* Sign binaries
* Dark mode
* Reproducible build

## Maintainer Notes - Make a Release
## Maintainer Notes for Releases

Downloadable MacOS binary:
Make a new release branch:
```bash
$ git checkout -b v0.x.x
```

Commit your changes, being sure to bump the version number in `setup.py`.

Basic tests:
```bash
$ black --check . && flake8 .
```

Make a downloadable MacOS binary to upload to GitHub:
```
$ ./make_macos_release.sh
```

Go to [GitHub release page](https://github.com/mflaxman/multiwallet/releases/new) and use tag version `v0.x.x` and target `v0.x.x` (target is the branch name which is independent of the tag).
Write a title, description, and drag the binary from the previous step.
Hit `Publish release`.

Update PyPI:
```
$ ./update_pypi.sh
```

Merge into main:
```
$ git checkout main
$ git merge v0.x.x
```
TODO: better to `merge` into `main` first?
36 changes: 36 additions & 0 deletions develop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#! /usr/bin/env bash

# Verbose printing
set -o xtrace

deactivate

# Abandon if anything errors
set -e;

# Remove old files
rm -rf .venv3/
rm -rf dist/
rm -rf build/
rm -rf multiwallet.egg-info/
find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf

# Virtualenv
python3 --version
# Install virtualenv (if not installed)
# python3 -m pip uninstall virtualenv -y
python3 -m pip install virtualenv
# Create virtualenv and install our software inside it
python3 -m virtualenv .venv3
source .venv3/bin/activate
# python3 -m pip uninstall pyinstaller -y
python3 -m pip install -r requirements.txt
python3 -m pip install --editable .
python3 -m pip freeze

# Hackey timer
# https://askubuntu.com/questions/1028924/how-do-i-use-seconds-inside-a-bash-script
hrs=$(( SECONDS/3600 ))
mins=$(( (SECONDS-hrs*3600)/60))
secs=$(( SECONDS-hrs*3600-mins*60 ))
printf 'Time spent: %02d:%02d:%02d\n' $hrs $mins $secs
2 changes: 1 addition & 1 deletion multiwallet_gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class MultiwalletApp(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle(
"Multiwallet - Stateless PSBT Multisig Wallet - ALPHA VERSION TESTNET ONLY"
"Multiwallet - Stateless PSBT Multisig Wallet - ALPHA VERSION"
)
self.setFixedWidth(800)

Expand Down
15 changes: 15 additions & 0 deletions multiwallet_gui/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,18 @@ def _is_libsec_enabled():
return True
except ModuleNotFoundError:
return False


BITCOIN_NETWORK_TOOLTIP = (
"Testnet is a great tool for practicing, as Testnet coins have no monetary value. "
"We recommend new users do a dry-run on Testnet before receiving real bitcoins."
"<br/><br/>"
"You can get free Testnet coins from several Testnet faucets."
"Testnet blocks"
)

BITCOIN_TESTNET_TOOLTIP = "Segwit Testnet addresses start with <i>tb1</i>..."

BITCOIN_MAINNET_TOOLTIP = (
"Regular bitcoin transaction" "Segwit Mainnet addresses start with <i>bc1</i>..."
)
2 changes: 0 additions & 2 deletions multiwallet_gui/receive.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,5 @@ def process_submit(self):
is_testnet=pubkeys_info["is_testnet"],
):
result = f"#{index}: {address}"
print("result", result)
self.addrResultsEdit.appendPlainText(result)
QApplication.processEvents() # needed to stream output (otherwise terrible UX)
print("done")
25 changes: 17 additions & 8 deletions multiwallet_gui/seedpicker.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#! /usr/bin/env bash

from multiwallet_gui.helper import _clean_submisission, _msgbox_err
from multiwallet_gui.helper import (
BITCOIN_NETWORK_TOOLTIP,
BITCOIN_TESTNET_TOOLTIP,
BITCOIN_MAINNET_TOOLTIP,
_clean_submisission,
_msgbox_err,
)

from PyQt5.QtWidgets import (
QLabel,
Expand Down Expand Up @@ -50,17 +56,20 @@ def __init__(self):
)
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"
"Something like this:\n\nzoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo"
)

# Network toggle
# https://www.tutorialspoint.com/pyqt/pyqt_qradiobutton_widget.htm
self.button_label = QLabel("<b>Bitcoin Network</b>")
self.button_label.setToolTip("We recommend practicing first on testnet.")
self.mainnet_button = QRadioButton("Mainnet (regular)")
# self.mainnet_button.toggled.connect(self.updateNetwork) # TODO: wire up any changes to reset the form
self.button_label.setToolTip(BITCOIN_NETWORK_TOOLTIP)

self.mainnet_button = QRadioButton("Mainnet")
self.mainnet_button.setToolTip(BITCOIN_MAINNET_TOOLTIP)
self.mainnet_button.setChecked(False)

self.testnet_button = QRadioButton("Testnet")
self.testnet_button.setToolTip(BITCOIN_TESTNET_TOOLTIP)
self.testnet_button.setChecked(True)

self.firstWordsSubmitButton = QPushButton("Calculate Full Seed")
Expand Down Expand Up @@ -143,8 +152,8 @@ def process_submit(self):
informative_text=err_str,
)

IS_TESTNET = self.testnet_button.isChecked()
if IS_TESTNET:
self.IS_TESTNET = self.testnet_button.isChecked()
if self.IS_TESTNET:
PATH = "m/48'/1'/0'/2'"
SLIP132_VERSION_BYTES = "02575483"
else:
Expand Down Expand Up @@ -174,7 +183,7 @@ def process_submit(self):
self.privResultsEdit.appendPlainText("\n".join(priv_to_display))

self.pubResultsLabel.setText(
f"<b>PUBLIC KEY INFO</b> - {'Testnet' if IS_TESTNET else 'Mainnet'}"
f"<b>PUBLIC KEY INFO</b> - {'Testnet' if self.IS_TESTNET else 'Mainnet'}"
)
self.pubResultsEdit.setHidden(False)
self.pubResultsEdit.appendPlainText("\n".join(pub_to_display))
130 changes: 96 additions & 34 deletions multiwallet_gui/send.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
#! /usr/bin/env bash

from multiwallet_gui.helper import _clean_submisission, _msgbox_err
from multiwallet_gui.helper import (
BITCOIN_NETWORK_TOOLTIP,
BITCOIN_TESTNET_TOOLTIP,
BITCOIN_MAINNET_TOOLTIP,
_clean_submisission,
_msgbox_err,
)
from PyQt5.QtWidgets import (
QVBoxLayout,
QWidget,
QLabel,
QPlainTextEdit,
QPushButton,
QRadioButton,
)


Expand Down Expand Up @@ -52,6 +59,31 @@ def __init__(self):
self.psbtEdit = QPlainTextEdit("")
self.psbtEdit.setPlaceholderText("Something like this:\n\ncHNidP8BAH0CAAAAA...")

# Network toggle
# https://www.tutorialspoint.com/pyqt/pyqt_qradiobutton_widget.htm
self.button_label = QLabel("<b>Bitcoin Network</b>")
self.button_label.setToolTip(BITCOIN_NETWORK_TOOLTIP)

self.infernetwork_button = QRadioButton("Smart Guess (default)")
self.infernetwork_button.setToolTip(
"Non-experts should choose this option."
"<br/><br/>"
"The current PSBT serialization format does not encode which network the transaction is on, but this software can usually infer the network based on the BIP32 path used. "
"If the address displayed is in the wrong format (<i>bc1...</i> vs <i>tb1...</i>) then you may need to manually select the network."
)
self.infernetwork_button.setChecked(True)

self.mainnet_button = QRadioButton("Mainnet")
self.mainnet_button.setToolTip(BITCOIN_MAINNET_TOOLTIP)
self.mainnet_button.setChecked(False)

self.testnet_button = QRadioButton("Testnet")
self.testnet_button.setToolTip(BITCOIN_TESTNET_TOOLTIP)
self.testnet_button.setChecked(False)

self.psbtSubmitButton = QPushButton("Decode Transaction")
self.psbtSubmitButton.clicked.connect(self.decode_psbt)

self.fullSeedLabel = QLabel("<b>Full 24-Word Seed Phrase</b> (optional)")
self.fullSeedLabel.setToolTip(
"Needed to sign the PSBT. You can first decode the transaction and inspect it without supplying your seed phrase."
Expand All @@ -61,20 +93,17 @@ def __init__(self):
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo"
)

self.fullSeedSubmitButton = QPushButton("Sign Transaction")
self.fullSeedSubmitButton.clicked.connect(self.sign_psbt)

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)

self.psbtSubmitButton = QPushButton("Decode Transaction")
self.psbtSubmitButton.clicked.connect(self.decode_psbt)

self.fullSeedSubmitButton = QPushButton("Sign Transaction")
self.fullSeedSubmitButton.clicked.connect(self.sign_psbt)
self.psbtDecodedROEdit = QPlainTextEdit("")
self.psbtDecodedROEdit.setReadOnly(True)
self.psbtDecodedROEdit.setHidden(True)

self.psbtSignedLabel = QLabel("")
self.psbtSignedLabel.setToolTip(
Expand All @@ -84,16 +113,23 @@ def __init__(self):
self.psbtSignedEdit.setReadOnly(True)
self.psbtSignedEdit.setHidden(True)

vbox.addWidget(self.psbtLabel)
vbox.addWidget(self.psbtEdit)
vbox.addWidget(self.psbtSubmitButton)
vbox.addWidget(self.fullSeedLabel)
vbox.addWidget(self.fullSeedEdit)
vbox.addWidget(self.fullSeedSubmitButton)
vbox.addWidget(self.psbtDecodedLabel)
vbox.addWidget(self.psbtDecodedEdit)
vbox.addWidget(self.psbtSignedLabel)
vbox.addWidget(self.psbtSignedEdit)
for widget in (
self.psbtLabel,
self.psbtEdit,
self.button_label,
self.infernetwork_button,
self.mainnet_button,
self.testnet_button,
self.psbtSubmitButton,
self.fullSeedLabel,
self.fullSeedEdit,
self.fullSeedSubmitButton,
self.psbtDecodedLabel,
self.psbtDecodedROEdit,
self.psbtSignedLabel,
self.psbtSignedEdit,
):
vbox.addWidget(widget)

self.setLayout(vbox)

Expand All @@ -106,14 +142,24 @@ def sign_psbt(self):
def process_psbt(self, sign_tx=True):
# Clear any previous submission in case of errors
self.psbtDecodedLabel.setText("")
self.psbtDecodedEdit.clear()
self.psbtDecodedEdit.setHidden(True)
self.psbtDecodedROEdit.clear()
self.psbtDecodedROEdit.setHidden(True)

self.psbtSignedLabel.setText("")
self.psbtSignedEdit.clear()
self.psbtSignedEdit.setHidden(True)
# TODO: why setText and not hide?

if self.infernetwork_button.isChecked():
PARSE_WITH_TESTNET = None
elif self.mainnet_button.isChecked():
PARSE_WITH_TESTNET = False
elif self.testnet_button.isChecked():
PARSE_WITH_TESTNET = True
else:
# This shouldn't be possible
raise Exception("Invalid Network Selection: No Radio Button Chosen")

psbt_str = _clean_submisission(self.psbtEdit.toPlainText())

if not psbt_str:
Expand All @@ -123,14 +169,25 @@ def process_psbt(self, sign_tx=True):
)

try:
psbt_obj = PSBT.parse_base64(b64=psbt_str, testnet=self.IS_TESTNET)
psbt_obj = PSBT.parse_base64(b64=psbt_str, testnet=PARSE_WITH_TESTNET)
except Exception as e:
return _msgbox_err(
main_text="PSBT Parse Error",
informative_text="Are you sure that's a PSBT?",
detailed_text=str(e),
)
TX_FEE_SATS = psbt_obj.tx_obj.fee()
if type(e) is ValueError and str(e) == "Mainnet/Testnet mixing":
# TODO: less hackey way to catch this error?
return _msgbox_err(
main_text="PSBT Network Error",
informative_text="The network you selected doesn't match the PSBT.",
detailed_text=str(e),
)
else:
return _msgbox_err(
main_text="PSBT Parse Error",
informative_text="Are you sure that's a valid PSBT?",
detailed_text=str(e),
)

# Parse TX
self.TX_FEE_SATS = psbt_obj.tx_obj.fee()
self.IS_TESTNET = psbt_obj.tx_obj.testnet

# Validate multisig transaction
# TODO: abstract some of this into buidl library?
Expand Down Expand Up @@ -288,18 +345,23 @@ def process_psbt(self, sign_tx=True):
"to",
spend_addr,
"with a fee of",
_format_satoshis(TX_FEE_SATS, in_btc=self.UNITS == "btc"),
f"({round(TX_FEE_SATS / TOTAL_INPUT_SATS * 100, 2)}% of spend)",
_format_satoshis(self.TX_FEE_SATS, in_btc=self.UNITS == "btc"),
f"({round(self.TX_FEE_SATS / TOTAL_INPUT_SATS * 100, 2)}% of spend)",
]
)
self.psbtDecodedLabel.setText("<b>Decoded Transaction Summary</b>")
self.psbtDecodedEdit.setHidden(False)
self.psbtDecodedEdit.appendPlainText(TX_SUMMARY)
self.psbtDecodedLabel.setText(
f"<b>Decoded Transaction Summary</b> - {'Testnet' if self.IS_TESTNET else 'Mainnet'}"
)
self.psbtDecodedROEdit.setHidden(False)
self.psbtDecodedROEdit.appendPlainText(TX_SUMMARY)

# TODO: surface this to user somehow
to_print = []
to_print.append("DETAILED VIEW")
to_print.append(f"TXID: {psbt_obj.tx_obj.id()}")
to_print.append(
f"Network: {'Testnet' if psbt_obj.tx_obj.testnet else 'Mainnet'}"
)
to_print.append("-" * 80)
to_print.append(f"{len(inputs_desc)} Input(s):")
for cnt, input_desc in enumerate(inputs_desc):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

setup(
name="multiwallet",
version="0.3.5",
version="0.3.6",
author="Michael Flaxman",
author_email="[email protected]",
description="Stateless multisig bitcoin wallet",
Expand Down

0 comments on commit 3b795c8

Please sign in to comment.