Merge pull request #8871 from SomberNight/202402_slip19_trezor
support SLIP-19 ownership proofs, for trezor-based Standard_Wallets
This commit is contained in:
@@ -71,9 +71,9 @@ if TYPE_CHECKING:
|
|||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
|
|
||||||
|
|
||||||
class MyMenu(QMenu):
|
class QMenuWithConfig(QMenu):
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config: 'SimpleConfig'):
|
||||||
QMenu.__init__(self)
|
QMenu.__init__(self)
|
||||||
self.setToolTipsVisible(True)
|
self.setToolTipsVisible(True)
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -113,7 +113,7 @@ class MyMenu(QMenu):
|
|||||||
|
|
||||||
|
|
||||||
def create_toolbar_with_menu(config: 'SimpleConfig', title):
|
def create_toolbar_with_menu(config: 'SimpleConfig', title):
|
||||||
menu = MyMenu(config)
|
menu = QMenuWithConfig(config)
|
||||||
toolbar_button = QToolButton()
|
toolbar_button = QToolButton()
|
||||||
toolbar_button.setIcon(read_QIcon("preferences.png"))
|
toolbar_button.setIcon(read_QIcon("preferences.png"))
|
||||||
toolbar_button.setMenu(menu)
|
toolbar_button.setMenu(menu)
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
|
|||||||
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX,
|
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX,
|
||||||
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX,
|
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX,
|
||||||
BlockingWaitingDialog, getSaveFileName, ColorSchemeItem,
|
BlockingWaitingDialog, getSaveFileName, ColorSchemeItem,
|
||||||
get_iconname_qrcode, VLine)
|
get_iconname_qrcode, VLine, WaitingDialog)
|
||||||
from .rate_limiter import rate_limited
|
from .rate_limiter import rate_limited
|
||||||
from .my_treeview import create_toolbar_with_menu
|
from .my_treeview import create_toolbar_with_menu, QMenuWithConfig
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
@@ -456,7 +456,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
self.desc = self.wallet.get_label_for_txid(txid) or None
|
self.desc = self.wallet.get_label_for_txid(txid) or None
|
||||||
self.setMinimumWidth(640)
|
self.setMinimumWidth(640)
|
||||||
|
|
||||||
self.psbt_only_widgets = [] # type: List[QWidget]
|
self.psbt_only_widgets = [] # type: List[Union[QWidget, QAction]]
|
||||||
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
self.setLayout(vbox)
|
self.setLayout(vbox)
|
||||||
@@ -502,15 +502,24 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
b.clicked.connect(self.close)
|
b.clicked.connect(self.close)
|
||||||
b.setDefault(True)
|
b.setDefault(True)
|
||||||
|
|
||||||
self.export_actions_menu = export_actions_menu = QMenu()
|
self.export_actions_menu = export_actions_menu = QMenuWithConfig(config=self.config)
|
||||||
self.add_export_actions_to_menu(export_actions_menu)
|
self.add_export_actions_to_menu(export_actions_menu)
|
||||||
export_actions_menu.addSeparator()
|
export_actions_menu.addSeparator()
|
||||||
export_submenu = export_actions_menu.addMenu(_("For CoinJoin; strip privates"))
|
export_option = export_actions_menu.addConfig(
|
||||||
self.add_export_actions_to_menu(export_submenu, gettx=self._gettx_for_coinjoin)
|
self.config.cv.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA)
|
||||||
self.psbt_only_widgets.append(export_submenu)
|
self.psbt_only_widgets.append(export_option)
|
||||||
export_submenu = export_actions_menu.addMenu(_("For hardware device; include xpubs"))
|
export_option = export_actions_menu.addConfig(
|
||||||
self.add_export_actions_to_menu(export_submenu, gettx=self._gettx_for_hardware_device)
|
self.config.cv.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS)
|
||||||
self.psbt_only_widgets.append(export_submenu)
|
self.psbt_only_widgets.append(export_option)
|
||||||
|
if self.wallet.has_support_for_slip_19_ownership_proofs():
|
||||||
|
export_option = export_actions_menu.addAction(
|
||||||
|
_('Include SLIP-19 ownership proofs'),
|
||||||
|
self._add_slip_19_ownership_proofs_to_tx)
|
||||||
|
export_option.setToolTip(_("Some cosigners (e.g. Trezor) might require this for coinjoins."))
|
||||||
|
self._export_option_slip19 = export_option
|
||||||
|
export_option.setCheckable(True)
|
||||||
|
export_option.setChecked(False)
|
||||||
|
self.psbt_only_widgets.append(export_option)
|
||||||
|
|
||||||
self.export_actions_button = QToolButton()
|
self.export_actions_button = QToolButton()
|
||||||
self.export_actions_button.setText(_("Share"))
|
self.export_actions_button.setText(_("Share"))
|
||||||
@@ -604,9 +613,17 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
# Override escape-key to close normally (and invoke closeEvent)
|
# Override escape-key to close normally (and invoke closeEvent)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def add_export_actions_to_menu(self, menu: QMenu, *, gettx: Callable[[], Transaction] = None) -> None:
|
def add_export_actions_to_menu(self, menu: QMenu) -> None:
|
||||||
if gettx is None:
|
def gettx() -> Transaction:
|
||||||
gettx = lambda: None
|
if not isinstance(self.tx, PartialTransaction):
|
||||||
|
return self.tx
|
||||||
|
tx = copy.deepcopy(self.tx)
|
||||||
|
if self.config.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS:
|
||||||
|
Network.run_from_another_thread(
|
||||||
|
tx.prepare_for_export_for_hardware_device(self.wallet))
|
||||||
|
if self.config.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA:
|
||||||
|
tx.prepare_for_export_for_coinjoin()
|
||||||
|
return tx
|
||||||
|
|
||||||
action = QAction(_("Copy to clipboard"), self)
|
action = QAction(_("Copy to clipboard"), self)
|
||||||
action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx()))
|
action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx()))
|
||||||
@@ -620,20 +637,19 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
action.triggered.connect(lambda: self.export_to_file(tx=gettx()))
|
action.triggered.connect(lambda: self.export_to_file(tx=gettx()))
|
||||||
menu.addAction(action)
|
menu.addAction(action)
|
||||||
|
|
||||||
def _gettx_for_coinjoin(self) -> PartialTransaction:
|
def _add_slip_19_ownership_proofs_to_tx(self):
|
||||||
if not isinstance(self.tx, PartialTransaction):
|
assert isinstance(self.tx, PartialTransaction)
|
||||||
raise Exception("Can only export partial transactions for coinjoins.")
|
def on_success(result):
|
||||||
tx = copy.deepcopy(self.tx)
|
self._export_option_slip19.setEnabled(False)
|
||||||
tx.prepare_for_export_for_coinjoin()
|
self.main_window.pop_top_level_window(self)
|
||||||
return tx
|
def on_failure(exc_info):
|
||||||
|
self._export_option_slip19.setChecked(False)
|
||||||
def _gettx_for_hardware_device(self) -> PartialTransaction:
|
self.main_window.on_error(exc_info)
|
||||||
if not isinstance(self.tx, PartialTransaction):
|
self.main_window.pop_top_level_window(self)
|
||||||
raise Exception("Can only export partial transactions for hardware device.")
|
task = partial(self.wallet.add_slip_19_ownership_proofs_to_tx, self.tx)
|
||||||
tx = copy.deepcopy(self.tx)
|
msg = _('Adding SLIP-19 ownership proofs to transaction...')
|
||||||
Network.run_from_another_thread(
|
self.main_window.push_top_level_window(self)
|
||||||
tx.prepare_for_export_for_hardware_device(self.wallet))
|
WaitingDialog(self, msg, task, on_success, on_failure)
|
||||||
return tx
|
|
||||||
|
|
||||||
def copy_to_clipboard(self, *, tx: Transaction = None):
|
def copy_to_clipboard(self, *, tx: Transaction = None):
|
||||||
if tx is None:
|
if tx is None:
|
||||||
|
|||||||
@@ -202,6 +202,12 @@ class KeyStore(Logger, ABC):
|
|||||||
def can_have_deterministic_lightning_xprv(self) -> bool:
|
def can_have_deterministic_lightning_xprv(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def has_support_for_slip_19_ownership_proofs(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_slip_19_ownership_proofs_to_tx(self, tx: 'PartialTransaction', *, password) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class Software_KeyStore(KeyStore):
|
class Software_KeyStore(KeyStore):
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,16 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||||||
with self.run_flow():
|
with self.run_flow():
|
||||||
return trezorlib.btc.sign_tx(self.client, *args, **kwargs)
|
return trezorlib.btc.sign_tx(self.client, *args, **kwargs)
|
||||||
|
|
||||||
|
@runs_in_hwd_thread
|
||||||
|
def get_ownership_id(self, *args, **kwargs):
|
||||||
|
with self.run_flow():
|
||||||
|
return trezorlib.btc.get_ownership_id(self.client, *args, **kwargs)
|
||||||
|
|
||||||
|
@runs_in_hwd_thread
|
||||||
|
def get_ownership_proof(self, *args, **kwargs):
|
||||||
|
with self.run_flow():
|
||||||
|
return trezorlib.btc.get_ownership_proof(self.client, *args, **kwargs)
|
||||||
|
|
||||||
@runs_in_hwd_thread
|
@runs_in_hwd_thread
|
||||||
def reset_device(self, *args, **kwargs):
|
def reset_device(self, *args, **kwargs):
|
||||||
with self.run_flow():
|
with self.run_flow():
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ try:
|
|||||||
TxInputType, TxOutputType, TxOutputBinType, TransactionType, AmountUnit)
|
TxInputType, TxOutputType, TxOutputBinType, TransactionType, AmountUnit)
|
||||||
|
|
||||||
from trezorlib.client import PASSPHRASE_ON_DEVICE
|
from trezorlib.client import PASSPHRASE_ON_DEVICE
|
||||||
|
import trezorlib.log
|
||||||
|
#trezorlib.log.enable_debug_output()
|
||||||
|
|
||||||
TREZORLIB = True
|
TREZORLIB = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -95,6 +97,39 @@ class TrezorKeyStore(Hardware_KeyStore):
|
|||||||
|
|
||||||
self.plugin.sign_transaction(self, tx, prev_tx)
|
self.plugin.sign_transaction(self, tx, prev_tx)
|
||||||
|
|
||||||
|
def has_support_for_slip_19_ownership_proofs(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_slip_19_ownership_proofs_to_tx(self, tx: 'PartialTransaction', password) -> None:
|
||||||
|
assert isinstance(tx, PartialTransaction)
|
||||||
|
client = self.get_client()
|
||||||
|
assert isinstance(client, TrezorClientBase), client
|
||||||
|
for txin in tx.inputs():
|
||||||
|
if txin.is_coinbase_input():
|
||||||
|
continue
|
||||||
|
if txin.is_complete() or not txin.is_mine:
|
||||||
|
continue
|
||||||
|
assert txin.scriptpubkey
|
||||||
|
desc = txin.script_descriptor
|
||||||
|
assert desc
|
||||||
|
trezor_multisig = None
|
||||||
|
if multi := desc.get_simple_multisig():
|
||||||
|
# trezor_multisig = self._make_multisig(multi)
|
||||||
|
raise Exception("multisig not supported for slip-19 ownership proof")
|
||||||
|
trezor_script_type = self.plugin.get_trezor_input_script_type(desc.to_legacy_electrum_script_type())
|
||||||
|
my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin)
|
||||||
|
if full_path:
|
||||||
|
trezor_address_n = full_path
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
proof, _proof_sig = client.get_ownership_proof(
|
||||||
|
coin_name=self.plugin.get_coin_name(),
|
||||||
|
n=trezor_address_n,
|
||||||
|
multisig=trezor_multisig,
|
||||||
|
script_type=trezor_script_type,
|
||||||
|
)
|
||||||
|
txin.slip_19_ownership_proof = proof
|
||||||
|
|
||||||
|
|
||||||
class TrezorInitSettings(NamedTuple):
|
class TrezorInitSettings(NamedTuple):
|
||||||
word_count: int
|
word_count: int
|
||||||
@@ -352,11 +387,13 @@ class TrezorPlugin(HW_PluginBase):
|
|||||||
assert isinstance(tx, PartialTransaction)
|
assert isinstance(tx, PartialTransaction)
|
||||||
assert isinstance(txin, PartialTxInput)
|
assert isinstance(txin, PartialTxInput)
|
||||||
assert keystore
|
assert keystore
|
||||||
if txin.is_complete() or not txin.is_mine:
|
if txin.is_complete() or not txin.is_mine: # we don't sign
|
||||||
txinputtype.script_type = InputScriptType.EXTERNAL
|
txinputtype.script_type = InputScriptType.EXTERNAL
|
||||||
assert txin.scriptpubkey
|
assert txin.scriptpubkey
|
||||||
txinputtype.script_pubkey = txin.scriptpubkey
|
txinputtype.script_pubkey = txin.scriptpubkey
|
||||||
else:
|
if not txin.is_mine and txin.slip_19_ownership_proof:
|
||||||
|
txinputtype.ownership_proof = txin.slip_19_ownership_proof
|
||||||
|
else: # we sign
|
||||||
desc = txin.script_descriptor
|
desc = txin.script_descriptor
|
||||||
assert desc
|
assert desc
|
||||||
if multi := desc.get_simple_multisig():
|
if multi := desc.get_simple_multisig():
|
||||||
|
|||||||
@@ -1083,6 +1083,14 @@ This will result in longer routes; it might increase your fees and decrease the
|
|||||||
'Download parent transactions from the network.\n'
|
'Download parent transactions from the network.\n'
|
||||||
'Allows filling in missing fee and input details.'),
|
'Allows filling in missing fee and input details.'),
|
||||||
)
|
)
|
||||||
|
GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA = ConfigVar(
|
||||||
|
'gui_qt_tx_dialog_export_strip_sensitive_metadata', default=False, type_=bool,
|
||||||
|
short_desc=lambda: _('For CoinJoin; strip privates'),
|
||||||
|
)
|
||||||
|
GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS = ConfigVar(
|
||||||
|
'gui_qt_tx_dialog_export_include_global_xpubs', default=False, type_=bool,
|
||||||
|
short_desc=lambda: _('For hardware device; include xpubs'),
|
||||||
|
)
|
||||||
GUI_QT_RECEIVE_TABS_INDEX = ConfigVar('receive_tabs_index', default=0, type_=int)
|
GUI_QT_RECEIVE_TABS_INDEX = ConfigVar('receive_tabs_index', default=0, type_=int)
|
||||||
GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool)
|
GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool)
|
||||||
GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar(
|
GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar(
|
||||||
|
|||||||
@@ -1289,6 +1289,7 @@ class PSBTInputType(IntEnum):
|
|||||||
BIP32_DERIVATION = 6
|
BIP32_DERIVATION = 6
|
||||||
FINAL_SCRIPTSIG = 7
|
FINAL_SCRIPTSIG = 7
|
||||||
FINAL_SCRIPTWITNESS = 8
|
FINAL_SCRIPTWITNESS = 8
|
||||||
|
SLIP19_OWNERSHIP_PROOF = 0x19
|
||||||
|
|
||||||
|
|
||||||
class PSBTOutputType(IntEnum):
|
class PSBTOutputType(IntEnum):
|
||||||
@@ -1386,6 +1387,7 @@ class PartialTxInput(TxInput, PSBTSection):
|
|||||||
self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path)
|
self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path)
|
||||||
self.redeem_script = None # type: Optional[bytes]
|
self.redeem_script = None # type: Optional[bytes]
|
||||||
self.witness_script = None # type: Optional[bytes]
|
self.witness_script = None # type: Optional[bytes]
|
||||||
|
self.slip_19_ownership_proof = None # type: Optional[bytes]
|
||||||
self._unknown = {} # type: Dict[bytes, bytes]
|
self._unknown = {} # type: Dict[bytes, bytes]
|
||||||
|
|
||||||
self._script_descriptor = None # type: Optional[Descriptor]
|
self._script_descriptor = None # type: Optional[Descriptor]
|
||||||
@@ -1439,6 +1441,7 @@ class PartialTxInput(TxInput, PSBTSection):
|
|||||||
'part_sigs': {pubkey.hex(): sig.hex() for pubkey, sig in self.part_sigs.items()},
|
'part_sigs': {pubkey.hex(): sig.hex() for pubkey, sig in self.part_sigs.items()},
|
||||||
'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))
|
'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))
|
||||||
for pubkey, (xfp, path) in self.bip32_paths.items()},
|
for pubkey, (xfp, path) in self.bip32_paths.items()},
|
||||||
|
'slip_19_ownership_proof': self.slip_19_ownership_proof.hex() if self.slip_19_ownership_proof else None,
|
||||||
'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()},
|
'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()},
|
||||||
})
|
})
|
||||||
return d
|
return d
|
||||||
@@ -1553,6 +1556,11 @@ class PartialTxInput(TxInput, PSBTSection):
|
|||||||
raise SerializationError(f"duplicate key: {repr(kt)}")
|
raise SerializationError(f"duplicate key: {repr(kt)}")
|
||||||
self.witness = val
|
self.witness = val
|
||||||
if key: raise SerializationError(f"key for {repr(kt)} must be empty")
|
if key: raise SerializationError(f"key for {repr(kt)} must be empty")
|
||||||
|
elif kt == PSBTInputType.SLIP19_OWNERSHIP_PROOF:
|
||||||
|
if self.slip_19_ownership_proof is not None:
|
||||||
|
raise SerializationError(f"duplicate key: {repr(kt)}")
|
||||||
|
self.slip_19_ownership_proof = val
|
||||||
|
if key: raise SerializationError(f"key for {repr(kt)} must be empty")
|
||||||
else:
|
else:
|
||||||
full_key = self.get_fullkey_from_keytype_and_key(kt, key)
|
full_key = self.get_fullkey_from_keytype_and_key(kt, key)
|
||||||
if full_key in self._unknown:
|
if full_key in self._unknown:
|
||||||
@@ -1579,6 +1587,8 @@ class PartialTxInput(TxInput, PSBTSection):
|
|||||||
wr(PSBTInputType.FINAL_SCRIPTSIG, self.script_sig)
|
wr(PSBTInputType.FINAL_SCRIPTSIG, self.script_sig)
|
||||||
if self.witness is not None:
|
if self.witness is not None:
|
||||||
wr(PSBTInputType.FINAL_SCRIPTWITNESS, self.witness)
|
wr(PSBTInputType.FINAL_SCRIPTWITNESS, self.witness)
|
||||||
|
if self.slip_19_ownership_proof:
|
||||||
|
wr(PSBTInputType.SLIP19_OWNERSHIP_PROOF, self.slip_19_ownership_proof)
|
||||||
for full_key, val in sorted(self._unknown.items()):
|
for full_key, val in sorted(self._unknown.items()):
|
||||||
key_type, key = self.get_keytype_and_key_from_fullkey(full_key)
|
key_type, key = self.get_keytype_and_key_from_fullkey(full_key)
|
||||||
wr(key_type, val, key=key)
|
wr(key_type, val, key=key)
|
||||||
|
|||||||
@@ -2447,6 +2447,12 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
|||||||
self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
|
self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
|
||||||
txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height
|
txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height
|
||||||
|
|
||||||
|
def has_support_for_slip_19_ownership_proofs(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]:
|
def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]:
|
||||||
if not self.is_mine(address):
|
if not self.is_mine(address):
|
||||||
return None
|
return None
|
||||||
@@ -3765,6 +3771,13 @@ class Standard_Wallet(Simple_Wallet, Deterministic_Wallet):
|
|||||||
pubkey = pubkeys[0]
|
pubkey = pubkeys[0]
|
||||||
return bitcoin.pubkey_to_address(self.txin_type, pubkey)
|
return bitcoin.pubkey_to_address(self.txin_type, pubkey)
|
||||||
|
|
||||||
|
def has_support_for_slip_19_ownership_proofs(self) -> bool:
|
||||||
|
return self.keystore.has_support_for_slip_19_ownership_proofs()
|
||||||
|
|
||||||
|
def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
|
||||||
|
tx.add_info_from_wallet(self)
|
||||||
|
self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None)
|
||||||
|
|
||||||
|
|
||||||
class Multisig_Wallet(Deterministic_Wallet):
|
class Multisig_Wallet(Deterministic_Wallet):
|
||||||
# generic m of n
|
# generic m of n
|
||||||
|
|||||||
Reference in New Issue
Block a user