Skip to content

Commit

Permalink
Branch 1.0.0 (#36)
Browse files Browse the repository at this point in the history
**Highlights**

- Full support for **Foundation Passport**, **Trezor**, **Keystone**
(full UR support)
- **SyncChat** for Label Synchronization backup and Multi-party Multisig
Collaboration can now be enabled in the last wallet setup wizard step
- Send dialog: Highlight projected block matching the selected fee rate 
- Balance Statement PDF Export
- Transaction diagram navigation by clicking on inputs and outputs

**UI Improvements and Bug Fixes**

- Improvements in **SyncChat** and **USB connection** reliability and UI
- Added a lot more extensive testing
- Better hardware signer images and icons thanks to
[BitcoinUI](https://github.com/reez/BitcoinUI)
  • Loading branch information
andreasgriffin authored Dec 20, 2024
1 parent e97093b commit 56d4fe6
Show file tree
Hide file tree
Showing 207 changed files with 37,502 additions and 30,961 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/build-mac-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Build Test DMG

on:
workflow_dispatch:
inputs:
tags:
description: 'Test Build DMG'
required: false
type: boolean

jobs:
build:
runs-on: macos-14
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history for all branches and tags



- name: Run build script
run: tools/build-mac/make_osx.sh


- name: Check for DMG file
run: |
if [ -z "$(find dist -type f -name '*.dmg')" ]; then
echo "dmg file is missing"
exit 1
fi
- name: Upload DMG Files from dist/
uses: actions/upload-artifact@v2
with:
name: dmgs
path: dist/*.dmg
4 changes: 3 additions & 1 deletion .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,6 @@ jobs:
- name: Run tests
run: |
poetry run pytest -vvv --log-cli-level=DEBUG --setup-show --maxfail=1
poetry run pytest -m 'not marker_qt_1 and not marker_qt_2' -vvv --log-cli-level=DEBUG --setup-show --maxfail=1
poetry run pytest -m 'marker_qt_1' -vvv --log-cli-level=DEBUG --setup-show --maxfail=1
poetry run pytest -m 'marker_qt_2' -vvv --log-cli-level=DEBUG --setup-show --maxfail=1
28 changes: 25 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@
"request": "launch",
"module": "pytest",
"args": [
"-vvv",
// "-vvv",
"tests/gui",
"--ignore=coding_tests",
"-s", // Disable all capturing of outputs
"--ignore=tools",
// "-s", // Disable all capturing of outputs
],
"console": "integratedTerminal",
"justMyCode": false
Expand All @@ -49,7 +50,7 @@
"args": [
"-vvv",
"--ignore=tests/gui",
"--ignore=coding_tests",
"--ignore=tools",
"-s", // Disable all capturing of outputs
],
"console": "integratedTerminal",
Expand All @@ -63,6 +64,7 @@
"-vvv",
"${file}",
"-s", // Disable all capturing of outputs
"--ignore=tools",
],
"console": "integratedTerminal",
"justMyCode": false
Expand Down Expand Up @@ -116,6 +118,26 @@
],
"console": "integratedTerminal",
"preLaunchTask": "Poetry Install" // label of the task
},{
"name": "Build windows",
"type": "python",
"request": "launch",
"program": "tools/build.py",
"args": [
"--targets", "windows",
],
"console": "integratedTerminal",
"preLaunchTask": "Poetry Install" // label of the task
},{
"name": "Build linux",
"type": "python",
"request": "launch",
"program": "tools/build.py",
"args": [
"--targets", "appimage",
],
"console": "integratedTerminal",
"preLaunchTask": "Poetry Install" // label of the task
},{
"name": "Build Linux (Current Files)",
"type": "python",
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- **Easy** Multisig-Wallet Setup
- Step-by-Step instructions for a secure MultiSig setup with PDF backup sheets
- Test transactions ensure that all hardware signers are ready
- Full support for [Coldcard](https://store.coinkite.com/promo/8BFF877000C34A86F410), [Coldcard Q](https://store.coinkite.com/promo/8BFF877000C34A86F410), [Bitbox02](https://shiftcrypto.ch/bitbox02/?ref=MOB4dk7gpm), [Blockstream Jade](https://store.blockstream.com/?code=XEocg5boS77D), and Specter DIY, supporting QR, USB, SD-card
- Full support for [Coldcard](https://store.coinkite.com/promo/8BFF877000C34A86F410), [Coldcard Q](https://store.coinkite.com/promo/8BFF877000C34A86F410), [Bitbox02](https://shiftcrypto.ch/bitbox02/?ref=MOB4dk7gpm), [Blockstream Jade](https://store.blockstream.com/?code=XEocg5boS77D), [Trezor](https://trezor.io), [Foundation Passport](https://foundation.xyz/passport), [Keystone](https://keyst.one), [Specter DIY](https://specter.solutions/hardware), using *QR*, *USB*, and *SD-card*
- **Secure**: Hardware signers only
- All wallets require hardware signers/wallets for safe seed storage
- Powered by **[BDK](https://github.com/bitcoindevkit/bdk)**
Expand All @@ -21,7 +21,7 @@
- **Sending** for non-technical users
- 1-click fee selection via mempool-blocks
- Automatic merging of utxos when fees are low
- **SyncTalk**:
- **SyncChat**:
- Encrypted cloud backup (via nostr) of labels
- Label synchronization between different computers
- Wallet chat and PSBTs sharing between different computers
Expand Down Expand Up @@ -80,7 +80,7 @@
- MicroSD (files)
- USB
- QR codes (enhanced QR code detection for Laptop cameras)
- Animated QR codes including [BBQr](https://bbqr.org/) and legacy formats
- Animated QR codes including [Coldcard/BBQr](https://bbqr.org/) and [UR](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md) format

- **Search and Filtering Options**

Expand Down
2 changes: 1 addition & 1 deletion bitcoin_safe/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# this is the source of the version information
__version__ = "1.0.0b4"
__version__ = "1.0.0b5"
18 changes: 14 additions & 4 deletions bitcoin_safe/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,15 @@ def __init__(self) -> None:
}
self.language_code: Optional[str] = None

def clean_recently_open_wallet(self):
this_deque = self.recently_open_wallets[self.network]
# clean deleted paths
for deque_item in list(this_deque):
if not Path(deque_item).exists():
this_deque.remove(deque_item)

def add_recently_open_wallet(self, file_path: str) -> None:
self.clean_recently_open_wallet()
self.recently_open_wallets[self.network].append(file_path)

@property
Expand Down Expand Up @@ -119,7 +127,7 @@ def dump(self) -> Dict[str, Any]:
return d

@classmethod
def from_dump(cls, dct: Dict, class_kwargs=None) -> "UserConfig":
def from_dump(cls, dct: Dict, class_kwargs: Dict | None = None) -> "UserConfig":
super()._from_dump(dct, class_kwargs=class_kwargs)
dct["recently_open_wallets"] = {
bdk.Network._member_map_[k]: UniqueDeque(v, maxlen=RECENT_WALLET_MAXLEN)
Expand All @@ -133,12 +141,14 @@ def from_dump(cls, dct: Dict, class_kwargs=None) -> "UserConfig":
# dct["config_dir"] = rel_home_path_to_abs_path(dct["config_dir"])
# dct["config_file"] = rel_home_path_to_abs_path(dct["config_file"])

u = cls()
instance = cls()

for k, v in dct.items():
if v is not None: # only overwrite the default value, if there is a value
setattr(u, k, v)
return u
setattr(instance, k, v)

instance.clean_recently_open_wallet()
return instance

@classmethod
def from_dump_migration(cls, dct: Dict[str, Any]) -> Dict[str, Any]:
Expand Down
151 changes: 151 additions & 0 deletions bitcoin_safe/descriptor_export_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#
# Bitcoin Safe
# Copyright (C) 2024 Andreas Griffin
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of version 3 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


import logging

from bitcoin_safe.gui.qt.util import save_file_dialog
from bitcoin_safe.hardware_signers import DescriptorExportType, DescriptorExportTypes
from bitcoin_safe.wallet import filename_clean

logger = logging.getLogger(__name__)

from typing import Optional

import bdkpython as bdk
from bitcoin_qr_tools.data import ConverterMultisigWalletExport
from bitcoin_qr_tools.signer_info import SignerInfo
from bitcoin_usb.address_types import DescriptorInfo

from .descriptors import MultipathDescriptor


class DescriptorExportTools:

@classmethod
def _get_coldcard_str(cls, wallet_id: str, multipath_descriptor: MultipathDescriptor) -> str:
return f"""# Coldcard descriptor export of wallet: {filename_clean( wallet_id, file_extension='', replace_spaces_by='_')}
{ multipath_descriptor.bdk_descriptors[0].as_string() }"""

@staticmethod
def _get_passport_str(wallet_id: str, descriptor_str: str, hardware_signer_name="Passport") -> str:
infos = DescriptorInfo.from_str(descriptor_str)
signer_infos = [
SignerInfo(
xpub=spk_provider.xpub,
fingerprint=spk_provider.fingerprint,
key_origin=spk_provider.key_origin,
derivation_path=spk_provider.derivation_path,
)
for spk_provider in infos.spk_providers
]
return ConverterMultisigWalletExport(
name=filename_clean(wallet_id, file_extension="", replace_spaces_by="_"),
threshold=infos.threshold,
address_type_short_name=infos.address_type.short_name.upper(),
signer_infos=signer_infos,
).to_custom_str(hardware_signer_name=hardware_signer_name)

@classmethod
def _get_keystone_str(cls, wallet_id: str, descriptor_str: str, network: bdk.Network) -> str:
return cls._get_passport_str(
wallet_id=wallet_id, descriptor_str=descriptor_str, hardware_signer_name="Keystone"
)

@classmethod
def _get_specter_diy_str(cls, wallet_id: str, descriptor_str: str) -> str:
simplified_descriptor = (
descriptor_str.split("#")[0].replace("/<0;1>/*", "").replace("0/*", "").replace("1/*", "")
)
return f"addwallet {filename_clean( wallet_id, file_extension='', replace_spaces_by='_')}&{simplified_descriptor}"

@classmethod
def _export_wallet(cls, wallet_id: str, s: str, descripor_type: DescriptorExportType) -> Optional[str]:
filename = save_file_dialog(
name_filters=["Text (*.txt)", "All Files (*.*)"],
default_suffix="txt",
default_filename=filename_clean(wallet_id, file_extension=".txt", replace_spaces_by="_")[:24],
window_title=f"Export Wallet for {descripor_type.display_name}",
)
if not filename:
return None

with open(filename, "w") as file:
file.write(s)
return filename

@classmethod
def get_export_str(
cls,
wallet_id: str,
multipath_descriptor: MultipathDescriptor,
network: bdk.Network,
descriptor_export_type: DescriptorExportType,
) -> str:
if descriptor_export_type.name == DescriptorExportTypes.text.name:
return multipath_descriptor.as_string()
elif descriptor_export_type.name == DescriptorExportTypes.coldcard.name:
return cls._get_coldcard_str(wallet_id=wallet_id, multipath_descriptor=multipath_descriptor)
elif descriptor_export_type.name == DescriptorExportTypes.passport.name:
return cls._get_passport_str(
wallet_id=wallet_id,
descriptor_str=multipath_descriptor.as_string(),
)
elif descriptor_export_type.name == DescriptorExportTypes.keystone.name:
return cls._get_keystone_str(
wallet_id=wallet_id, descriptor_str=multipath_descriptor.as_string(), network=network
)
elif descriptor_export_type.name == DescriptorExportTypes.specterdiy.name:
return cls._get_specter_diy_str(
wallet_id=wallet_id, descriptor_str=multipath_descriptor.as_string()
)
else:
raise NotImplementedError(f"Cannot export descritpor for type {descriptor_export_type}")

@classmethod
def export(
cls,
wallet_id: str,
multipath_descriptor: MultipathDescriptor,
network: bdk.Network,
descripor_type: DescriptorExportType,
):
if descripor_type.name not in [t.name for t in DescriptorExportTypes.as_list()]:
logger.error(f"Cannot export the descriptor for {descripor_type}")
return

cls._export_wallet(
wallet_id=wallet_id,
s=cls.get_export_str(
wallet_id=wallet_id,
multipath_descriptor=multipath_descriptor,
network=network,
descriptor_export_type=descripor_type,
),
descripor_type=descripor_type,
)
13 changes: 12 additions & 1 deletion bitcoin_safe/dynamic_lib_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def get_packaged_libsecp256k1_path() -> str | None:

for name in ["libsecp256k1.so.0.0.0", "libsecp256k1.so.0"]:
lib_path = Path(__file__).parent.parent.parent.parent / name
logger.info(f"Searching for {name} in {lib_path.absolute()}")
logger.debug(f"Searching for {name} in {lib_path.absolute()}")
if lib_path.exists():
return str(lib_path)

Expand All @@ -86,6 +86,17 @@ def get_packaged_libsecp256k1_path() -> str | None:
if lib_path.exists():
return str(lib_path)

elif platform.system() == "Darwin":
# for exe the dlls are packages in the same folder as dynamic_lib_load.py
# packaged in setup: __file__ = bitcoin_safe/dynamic_lib_load.pyc
# the dll is in: C:/Program Files/Bitcoin Safe/_internals/libsecp256k1.2.dylib
for name in ["libsecp256k1.2.dylib"]:
# logger.info(f"file in {Path(__file__).absolute()}")
lib_path = Path(__file__).parent.parent / name
logger.info(f"Searching for {name} in {lib_path.absolute()}")
if lib_path.exists():
return str(lib_path)

return None


Expand Down
4 changes: 2 additions & 2 deletions bitcoin_safe/fx.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@

logger = logging.getLogger(__name__)


from PyQt6.QtCore import QLocale, QObject, pyqtSignal

from .mempool import threaded_fetch
from .signals import TypedPyQtSignalNo


class FX(QObject, ThreadingManager):
signal_data_updated = pyqtSignal()
signal_data_updated: TypedPyQtSignalNo = pyqtSignal() # type: ignore

def __init__(self, threading_parent: ThreadingManager | None = None) -> None:
super().__init__(threading_parent=threading_parent) # type: ignore
Expand Down
Loading

0 comments on commit 56d4fe6

Please sign in to comment.