diff --git a/electrum/commands.py b/electrum/commands.py index b72d937fd..d2481318a 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -33,6 +33,7 @@ import base64 import operator import asyncio import inspect +from collections import defaultdict from functools import wraps, partial from itertools import repeat from decimal import Decimal @@ -393,21 +394,35 @@ class Commands: tx.sign(keypairs) return tx.serialize() - @command('wp') - async def signtransaction(self, tx, privkey=None, password=None, wallet: Abstract_Wallet = None): - """Sign a transaction. The wallet keys will be used unless a private key is provided.""" - # TODO this command should be split in two... (1) *_with_wallet, (2) *_with_privkey + @command('') + async def signtransaction_with_privkey(self, tx, privkey): + """Sign a transaction. The provided list of private keys will be used to sign the transaction.""" tx = tx_from_any(tx) - if privkey: - txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey) - pubkey = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed) - for txin in tx.inputs(): - if txin.address and txin.address == bitcoin.pubkey_to_address(txin_type, pubkey.hex()): + + txins_dict = defaultdict(list) + for txin in tx.inputs(): + txins_dict[txin.address].append(txin) + + if not isinstance(privkey, list): + privkey = [privkey] + + for priv in privkey: + txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv) + pubkey = ecc.ECPrivkey(priv2).get_public_key_bytes(compressed=compressed) + address = bitcoin.pubkey_to_address(txin_type, pubkey.hex()) + if address in txins_dict.keys(): + for txin in txins_dict[address]: txin.pubkeys = [pubkey] txin.script_type = txin_type - tx.sign({pubkey.hex(): (privkey2, compressed)}) - else: - wallet.sign_transaction(tx, password) + tx.sign({pubkey.hex(): (priv2, compressed)}) + + return tx.serialize() + + @command('wp') + async def signtransaction(self, tx, password=None, wallet: Abstract_Wallet = None): + """Sign a transaction. The wallet keys will be used to sign the transaction.""" + tx = tx_from_any(tx) + wallet.sign_transaction(tx, password) return tx.serialize() @command('') diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py index 05f3c7586..4909bce62 100644 --- a/electrum/tests/test_commands.py +++ b/electrum/tests/test_commands.py @@ -11,6 +11,7 @@ from electrum.simple_config import SimpleConfig from electrum.transaction import Transaction, TxOutput, tx_from_any from . import TestCaseForTestnet, ElectrumTestCase +from .test_wallet_vertical import WalletIntegrityHelper class TestCommands(ElectrumTestCase): @@ -249,10 +250,29 @@ class TestCommandsTestnet(TestCaseForTestnet): @mock.patch.object(wallet.Abstract_Wallet, 'save_db') def test_signtransaction_without_wallet(self, mock_save_db): - dummy_wallet = restore_wallet_from_text( - '9dk', gap_limit=2, path='if_this_exists_mocking_failed_648151893', config=self.config)['wallet'] cmds = Commands(config=self.config) unsigned_tx = "70736274ff0100a0020000000221d3645ba44f33fff6fe2666dc080279bc34b531c66888729712a80b204a32a10100000000fdffffffdd7f90d51acf98dc45ad7489316a983868c75e16bf14ffeb9eae01603a7b4da40100000000fdffffff02e8030000000000001976a9149a9ec2b35a7660c80dae38dd806fdf9b0fde68fd88ac74c11000000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88aca79b1d00000100e102000000018ba8cf9f0ff0b44c389e4a1cd25c0770636d95ccef161e313542647d435a5fd0000000006a4730440220373b3989905177f2e36d7e3d02b967d03092747fe7bbd3ba7b2c24623a88538c02207be79ee1d981060c2be6783f4946ce1bda1f64671b349ef14a4a6fecc047a71e0121030de43c5ed4c6272d20ce3becf3fb7afd5c3ccfb5d58ddfdf3047981e0b005e0dfdffffff02c0010700000000001976a9141cd3eb65bce2cae9f54544b65e46b3ad1f0b187288ac40420f00000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88ac979b1d00000100e102000000014e39236158716e91b0b2170ebe9d6b359d139e9ebfff163f2bafd0bec9890d04000000006a473044022070340deb95ca25ef86c4c7a9539b5c8f7b8351941635450311f914cd9c2f45ea02203fa7576e032ab5ae4763c78f5c2124573213c956286fd766582d9462515dc6540121033f6737e40a3a6087bc58bc5b82b427f9ed26d710b8fe2f70bfdd3d62abebcf74fdffffff02e8030000000000001976a91490350959750b3b38e451df16bd5957b7649bf5d288acac840100000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88ac979b1d00000000" privkey = "cVtE728tULSA4gut4QWxo218q6PRsXHQAv84SXix83cuvScvGd1H" self.assertEqual("020000000221d3645ba44f33fff6fe2666dc080279bc34b531c66888729712a80b204a32a1010000006a47304402205b30e188e30c846f98dacc714c16b7cd3a58a3fa24973d289683c9d32813e24c0220153855a29e96fb083084417ba3e3873ccaeb08435dad93773ab60716f94a36160121033f6737e40a3a6087bc58bc5b82b427f9ed26d710b8fe2f70bfdd3d62abebcf74fdffffffdd7f90d51acf98dc45ad7489316a983868c75e16bf14ffeb9eae01603a7b4da4010000006a473044022010daa3dadf53bdcb071c6eff6b8787e3f675ed61feb4fef72d0bf9d99c0162f802200e73abd880b6f2ee5fe8c0abab731f1dddeb0f60df5e050a79c365bd718da1c80121033f6737e40a3a6087bc58bc5b82b427f9ed26d710b8fe2f70bfdd3d62abebcf74fdffffff02e8030000000000001976a9149a9ec2b35a7660c80dae38dd806fdf9b0fde68fd88ac74c11000000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88aca79b1d00", - cmds._run('signtransaction', (), tx=unsigned_tx, privkey=privkey, wallet=dummy_wallet)) + cmds._run('signtransaction_with_privkey', (), tx=unsigned_tx, privkey=privkey)) + + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_signtransaction_with_wallet(self, mock_save_db): + wallet = restore_wallet_from_text('bitter grass shiver impose acquire brush forget axis eager alone wine silver', + gap_limit=2, + path='if_this_exists_mocking_failed_648151893', + config=self.config)['wallet'] + + # bootstrap wallet1 + funding_tx = Transaction('01000000014576dacce264c24d81887642b726f5d64aa7825b21b350c7b75a57f337da6845010000006b483045022100a3f8b6155c71a98ad9986edd6161b20d24fad99b6463c23b463856c0ee54826d02200f606017fd987696ebbe5200daedde922eee264325a184d5bbda965ba5160821012102e5c473c051dae31043c335266d0ef89c1daab2f34d885cc7706b267f3269c609ffffffff0240420f00000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c42a2ddb90e000000001976a914c384950342cb6f8df55175b48586838b03130fad88ac00000000') + funding_txid = funding_tx.txid() + funding_output_value = 1000000 + self.assertEqual('add2535aedcbb5ba79cc2260868bb9e57f328738ca192937f2c92e0e94c19203', funding_txid) + wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + cmds = Commands(config=self.config) + + unsigned_tx = "cHNidP8BAHECAAAAAQOSwZQOLsnyNykZyjiHMn/luYuGYCLMebq1y+1aU9KtAAAAAAD+////AigjAAAAAAAAFgAUaQtZqBQGAvsjzCkE7OnMTa82EFIwGw8AAAAAABYAFKwOLSKSAL/7IWftb9GWrvnWh9i7AAAAAAABAN8BAAAAAUV22sziZMJNgYh2Qrcm9dZKp4JbIbNQx7daV/M32mhFAQAAAGtIMEUCIQCj+LYVXHGpitmYbt1hYbINJPrZm2RjwjtGOFbA7lSCbQIgD2BgF/2YdpbrvlIA2u3eki7uJkMloYTVu9qWW6UWCCEBIQLlxHPAUdrjEEPDNSZtDvicHaqy802IXMdwayZ/MmnGCf////8CQEIPAAAAAAAWABSKKL3bf2GGS9z1iyrRPVrrOrw8QqLduQ4AAAAAGXapFMOElQNCy2+N9VF1tIWGg4sDEw+tiKwAAAAAIgYDD67ptKJbfbggI8qYkZJxLN1MtT09kzhZHHkJ5YGuHAwQsuNafQAAAIAAAAAAAAAAAAAiAgKFhOeJ459BORsvJ4UsoYq+wGpUEcIb41D+1h7scSDeUxCy41p9AAAAgAEAAAAAAAAAAAA=" + + self.assertEqual("020000000001010392c1940e2ec9f2372919ca3887327fe5b98b866022cc79bab5cbed5a53d2ad0000000000feffffff022823000000000000160014690b59a8140602fb23cc2904ece9cc4daf361052301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb02473044022027e1e37172e52b2d84106663cff5bcf6e447dcb41f6483f99584cfb4de2785f4022005c72f6324ad130c78fca43fe5fc565526d1723f2c9dc3efea78f66d7ae9d4360121030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c00000000", + cmds._run('signtransaction', (), tx=unsigned_tx, wallet=wallet)) diff --git a/run_electrum b/run_electrum index c44650a3a..3a086dc4f 100755 --- a/run_electrum +++ b/run_electrum @@ -120,10 +120,6 @@ def init_cmdline(config_options, wallet_path, server, *, config: 'SimpleConfig') cmdname = config.get('cmd') cmd = known_commands[cmdname] - if cmdname == 'signtransaction' and config.get('privkey'): - cmd.requires_wallet = False - cmd.requires_password = False - if cmdname in ['payto', 'paytomany'] and config.get('unsigned'): cmd.requires_password = False