class TestSignTx(DeviceTestCase): def setUp(self): self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format( self.rpc_userpass)) if '{}_test'.format(self.type) not in self.rpc.listwallets(): self.rpc.createwallet('{}_test'.format(self.type), True) self.wrpc = AuthServiceProxy( 'http://{}@127.0.0.1:18443/wallet/{}_test'.format( self.rpc_userpass, self.type)) self.wpk_rpc = AuthServiceProxy( 'http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) if '--testnet' not in self.dev_args: self.dev_args.append('--testnet') def _test_signtx(self, input_type, multisig): # Import some keys to the watch only wallet and send coins to them keypool_desc = process_commands( self.dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '30', '40']) import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) keypool_desc = process_commands( self.dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--internal', '30', '40']) import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') wpkh_addr = self.wrpc.getnewaddress('', 'bech32') pkh_addr = self.wrpc.getnewaddress('', 'legacy') self.wrpc.importaddress(wpkh_addr) self.wrpc.importaddress(pkh_addr) # pubkeys to construct 2-of-3 multisig descriptors for import sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr) wpkh_info = self.wrpc.getaddressinfo(wpkh_addr) pkh_info = self.wrpc.getaddressinfo(pkh_addr) # Get origin info/key pair so wallet doesn't forget how to # sign with keys post-import pubkeys = [sh_wpkh_info['desc'][8:-2],\ wpkh_info['desc'][5:-1],\ pkh_info['desc'][4:-1]] sh_multi_desc = { 'desc': 'sh(multi(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))', "timestamp": "now", "label": "shmulti" } sh_wsh_multi_desc = { 'desc': 'sh(wsh(multi(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))', "timestamp": "now", "label": "shwshmulti" } # re-order pubkeys to allow import without "already have private keys" error wsh_multi_desc = { 'desc': 'wsh(multi(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))', "timestamp": "now", "label": "wshmulti" } multi_result = self.wrpc.importmulti( [sh_multi_desc, sh_wsh_multi_desc, wsh_multi_desc]) self.assertTrue(multi_result[0]['success']) self.assertTrue(multi_result[1]['success']) self.assertTrue(multi_result[2]['success']) sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti").popitem()[0] sh_wsh_multi_addr = self.wrpc.getaddressesbylabel( "shwshmulti").popitem()[0] wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti").popitem()[0] send_amount = 2 number_inputs = 0 # Single-sig if input_type == 'segwit' or input_type == 'all': self.wpk_rpc.sendtoaddress(sh_wpkh_addr, send_amount) self.wpk_rpc.sendtoaddress(wpkh_addr, send_amount) number_inputs += 2 if input_type == 'legacy' or input_type == 'all': self.wpk_rpc.sendtoaddress(pkh_addr, send_amount) number_inputs += 1 # Now do segwit/legacy multisig if multisig: if input_type == 'legacy' or input_type == 'all': self.wpk_rpc.sendtoaddress(sh_multi_addr, send_amount) number_inputs += 1 if input_type == 'segwit' or input_type == 'all': self.wpk_rpc.sendtoaddress(wsh_multi_addr, send_amount) self.wpk_rpc.sendtoaddress(sh_wsh_multi_addr, send_amount) number_inputs += 2 self.wpk_rpc.generatetoaddress(6, self.wpk_rpc.getnewaddress()) # Spend different amounts, requiring 1 to 3 inputs for i in range(number_inputs): # Create a psbt spending the above psbt = self.wrpc.walletcreatefundedpsbt( [], [{ self.wpk_rpc.getnewaddress(): (i + 1) * send_amount }], 0, { 'includeWatching': True, 'subtractFeeFromOutputs': [0] }, True) sign_res = process_commands(self.dev_args + ['signtx', psbt['psbt']]) finalize_res = self.wrpc.finalizepsbt(sign_res['psbt']) self.assertTrue(finalize_res['complete']) self.wrpc.sendrawtransaction(finalize_res['hex']) # Test wrapper to avoid mixed-inputs signing for Ledger def test_signtx(self): supports_mixed = {'coldcard', 'trezor'} supports_multisig = {'ledger', 'trezor'} if self.type not in supports_mixed: self._test_signtx("legacy", self.type in supports_multisig) self._test_signtx("segwit", self.type in supports_multisig) else: self._test_signtx("all", self.type in supports_multisig)
class TestSignTx(DeviceTestCase): def setUp(self): self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format( self.rpc_userpass)) if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): self.rpc.createwallet('{}_test'.format(self.full_type), True) self.wrpc = AuthServiceProxy( 'http://{}@127.0.0.1:18443/wallet/{}_test'.format( self.rpc_userpass, self.full_type)) self.wpk_rpc = AuthServiceProxy( 'http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) if '--testnet' not in self.dev_args: self.dev_args.append('--testnet') self.emulator.start() def tearDown(self): self.emulator.stop() def _generate_and_finalize(self, unknown_inputs, psbt): if not unknown_inputs: # Just do the normal signing process to test "all inputs" case sign_res = self.do_command(self.dev_args + ['signtx', psbt['psbt']]) finalize_res = self.wrpc.finalizepsbt(sign_res['psbt']) else: # Sign only input one on first pass # then rest on second pass to test ability to successfully # ignore inputs that are not its own. Then combine both # signing passes to ensure they are actually properly being # partially signed at each step. first_psbt = PSBT() first_psbt.deserialize(psbt['psbt']) second_psbt = PSBT() second_psbt.deserialize(psbt['psbt']) # Blank master fingerprint to make hww fail to sign # Single input PSBTs will be fully signed by first signer for psbt_input in first_psbt.inputs[1:]: for pubkey, path in psbt_input.hd_keypaths.items(): psbt_input.hd_keypaths[pubkey] = (0, ) + path[1:] for pubkey, path in second_psbt.inputs[0].hd_keypaths.items(): second_psbt.inputs[0].hd_keypaths[pubkey] = (0, ) + path[1:] single_input = len(first_psbt.inputs) == 1 # Process the psbts first_psbt = first_psbt.serialize() second_psbt = second_psbt.serialize() # First will always have something to sign first_sign_res = self.do_command(self.dev_args + ['signtx', first_psbt]) self.assertTrue(single_input == self.wrpc.finalizepsbt( first_sign_res['psbt'])['complete']) # Second may have nothing to sign (1 input case) # and also may throw an error(e.g., ColdCard) second_sign_res = self.do_command(self.dev_args + ['signtx', second_psbt]) if 'psbt' in second_sign_res: self.assertTrue(not self.wrpc.finalizepsbt( second_sign_res['psbt'])['complete']) combined_psbt = self.wrpc.combinepsbt( [first_sign_res['psbt'], second_sign_res['psbt']]) else: self.assertTrue('error' in second_sign_res) combined_psbt = first_sign_res['psbt'] finalize_res = self.wrpc.finalizepsbt(combined_psbt) self.assertTrue(finalize_res['complete']) self.assertTrue( self.wrpc.testmempoolaccept([finalize_res['hex'] ])[0]["allowed"]) return finalize_res['hex'] def _test_signtx(self, input_type, multisig): # Import some keys to the watch only wallet and send coins to them keypool_desc = self.do_command( self.dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '30', '40']) import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) keypool_desc = self.do_command( self.dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--internal', '30', '40']) import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') wpkh_addr = self.wrpc.getnewaddress('', 'bech32') pkh_addr = self.wrpc.getnewaddress('', 'legacy') self.wrpc.importaddress(wpkh_addr) self.wrpc.importaddress(pkh_addr) # pubkeys to construct 2-of-3 multisig descriptors for import sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr) wpkh_info = self.wrpc.getaddressinfo(wpkh_addr) pkh_info = self.wrpc.getaddressinfo(pkh_addr) # Get origin info/key pair so wallet doesn't forget how to # sign with keys post-import pubkeys = [sh_wpkh_info['desc'][8:-11],\ wpkh_info['desc'][5:-10],\ pkh_info['desc'][4:-10]] # Get the descriptors with their checksums sh_multi_desc = self.wrpc.getdescriptorinfo('sh(multi(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor'] sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(multi(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor'] wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(multi(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor'] sh_multi_import = { 'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti" } sh_wsh_multi_import = { 'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti" } # re-order pubkeys to allow import without "already have private keys" error wsh_multi_import = { 'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti" } multi_result = self.wrpc.importmulti( [sh_multi_import, sh_wsh_multi_import, wsh_multi_import]) self.assertTrue(multi_result[0]['success']) self.assertTrue(multi_result[1]['success']) self.assertTrue(multi_result[2]['success']) sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti").popitem()[0] sh_wsh_multi_addr = self.wrpc.getaddressesbylabel( "shwshmulti").popitem()[0] wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti").popitem()[0] in_amt = 3 out_amt = in_amt // 3 number_inputs = 0 # Single-sig if input_type == 'segwit' or input_type == 'all': self.wpk_rpc.sendtoaddress(sh_wpkh_addr, in_amt) self.wpk_rpc.sendtoaddress(wpkh_addr, in_amt) number_inputs += 2 if input_type == 'legacy' or input_type == 'all': self.wpk_rpc.sendtoaddress(pkh_addr, in_amt) number_inputs += 1 # Now do segwit/legacy multisig if multisig: if input_type == 'legacy' or input_type == 'all': self.wpk_rpc.sendtoaddress(sh_multi_addr, in_amt) number_inputs += 1 if input_type == 'segwit' or input_type == 'all': self.wpk_rpc.sendtoaddress(wsh_multi_addr, in_amt) self.wpk_rpc.sendtoaddress(sh_wsh_multi_addr, in_amt) number_inputs += 2 self.wpk_rpc.generatetoaddress(6, self.wpk_rpc.getnewaddress()) # Spend different amounts, requiring 1 to 3 inputs for i in range(number_inputs): # Create a psbt spending the above if i == number_inputs - 1: self.assertTrue((i + 1) * in_amt == self.wrpc.getbalance("*", 0, True)) psbt = self.wrpc.walletcreatefundedpsbt( [], [{ self.wpk_rpc.getnewaddress('', 'legacy'): (i + 1) * out_amt }, { self.wpk_rpc.getnewaddress('', 'p2sh-segwit'): (i + 1) * out_amt }, { self.wpk_rpc.getnewaddress('', 'bech32'): (i + 1) * out_amt }], 0, { 'includeWatching': True, 'subtractFeeFromOutputs': [0, 1, 2] }, True) # Sign with unknown inputs in two steps self._generate_and_finalize(True, psbt) # Sign all inputs all at once final_tx = self._generate_and_finalize(False, psbt) # Send off final tx to sweep the wallet self.wrpc.sendrawtransaction(final_tx) # Test wrapper to avoid mixed-inputs signing for Ledger def test_signtx(self): supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey'} supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey'} if self.full_type not in supports_mixed: self._test_signtx("legacy", self.full_type in supports_multisig) self._test_signtx("segwit", self.full_type in supports_multisig) else: self._test_signtx("all", self.full_type in supports_multisig) # Make a huge transaction which might cause some problems with different interfaces def test_big_tx(self): # make a huge transaction that is unrelated to the hardware wallet outputs = [] num_inputs = 60 for i in range(0, num_inputs): outputs.append({self.wpk_rpc.getnewaddress('', 'legacy'): 0.001}) psbt = self.wpk_rpc.walletcreatefundedpsbt([], outputs, 0, {}, True)['psbt'] psbt = self.wpk_rpc.walletprocesspsbt(psbt)['psbt'] tx = self.wpk_rpc.finalizepsbt(psbt)['hex'] txid = self.wpk_rpc.sendrawtransaction(tx) inputs = [] for i in range(0, num_inputs): inputs.append({'txid': txid, 'vout': i}) psbt = self.wpk_rpc.walletcreatefundedpsbt( inputs, [{ self.wpk_rpc.getnewaddress('', 'legacy'): 0.001 * num_inputs }], 0, {'subtractFeeFromOutputs': [0]}, True)['psbt'] # For cli, this should throw an exception try: result = self.do_command(self.dev_args + ['signtx', psbt]) if self.interface == 'cli': self.fail('Big tx did not cause CLI to error') if self.type == 'coldcard': self.assertEqual(result['code'], -7) else: self.assertNotIn('code', result) self.assertNotIn('error', result) except OSError as e: if self.interface == 'cli': pass
class Wallet: def __init__(self, network: str, datadir: str): global network_port_map_g, ltc_network_port_map_g, transfer_info_map_g self.rpc_user = input('RPC Username: '******'RPC Password: '******'Username: '******'Select Crypto(0 => Bitcoin or 1 => Litecoin): '))] self.rpc_port = network_port_map_g[self.crypto][network] self.rpc_connection = AuthServiceProxy( "http://%s:%[email protected]:%d" % (self.rpc_user, self.rpc_password, self.rpc_port), timeout=10000) self.network = network self.transfer_info_filepath = os.path.join( datadir, '%s.%s.%s.json' % (transfer_info_map_g[network], self.crypto, user)) self.user = user def isAddressUnused(self, address: str): if self.crypto == 'bitcoin': res = requests.get('https://blockchain.info/rawaddr/' + address) jsonobj = json.loads(res.text) return (jsonobj['total_received'] == 0) if self.crypto == 'litecoin': res = requests.get('https://chain.so/api/v2/address/LTC/' + address) jsonobj = json.loads(res.text) return (jsonobj['data']['total_txs'] == 0) def setUnusedAddresses(self): self.unused_list = [ address for address in self.jsonobj['Addresses'] if self.isAddressUnused(address) == True ] def setUnusedAddressesTest(self): unspent_list = self.rpc_connection.listunspent() if len(unspent_list) > 0: inuse_addresses = [unspent['address'] for unspent in unspent_list] index = 0 for inuse_address in inuse_addresses: if inuse_address in self.jsonobj['Addresses']: new_index = self.jsonobj['Addresses'].index(inuse_address) index = new_index if new_index > index else index self.unused_list = copy(self.jsonobj['Addresses'][index + 1:]) else: self.unused_list = copy(self.jsonobj['Addresses']) # print('unused list = %s' % self.unused_list) def getNextAddresses(self): if network == 'regtest': self.setUnusedAddressesTest() else: self.setUnusedAddresses() count = int(input('Enter Number of Unused Addresses: ')) addresses = self.unused_list[0:count] return addresses # def getTargetAddresses(self): # if network == 'regtest': # self.setUnusedAddressesTest() # else: # self.setUnusedAddresses() # # out_count = int(input('Enter Number of Target Addresses: ')) # # tx_out = [] # # unused_addresses = copy(self.unused_list) # # for i in range(out_count): # address_value = {} # choice = (input('Scan QR code [Y/n]: ') or 'Y').lower() # if choice == 'y': # qrcode = qrutils.scanQRCode() # if ':' in qrcode and qrcode.split(':')[0] != self.crypto: # print('Address belong to different cryptocurrency i.e. %s' % qrcode.split(':')[0]) # exit() # address = qrcode.split(':')[1].split('?')[0] if ':' in qrcode else qrcode.split('?')[0] # value = float(qrcode.split('?')[1].split('=')[1]) if '?' in qrcode else float(input('Enter Btc/Ltc: ')) # elif choice == 'n': # address = input('Enter Target Address: ') # value = float(input('Enter Bitcoins: ')) # else: # print('Invalid entry') # exit() # address_value[address] = value # # if address in unused_addresses: # unused_addresses.remove(address) # # tx_out.append(address_value) # # if len(set(tx_out)) != out_count: # print('Address repetition is not allowed in target') # exit() # # change_address = unused_addresses[0] # print('change address: %s' % change_address) # return tx_out, change_address def getTargetAddresses(self): if network == 'regtest': self.setUnusedAddressesTest() else: self.setUnusedAddresses() out_count = int(input('Enter Number of Target Addresses: ')) tx_out = [] unused_addresses = copy(self.unused_list) tx_out = {} for i in range(out_count): choice = (input('Scan QR code [Y/n]: ') or 'Y').lower() if choice == 'y': qrcode = qrutils.scanQRCode() if ':' in qrcode and qrcode.split(':')[0] != self.crypto: print( 'Address belong to different cryptocurrency i.e. %s' % qrcode.split(':')[0]) exit() address = qrcode.split(':')[1].split( '?')[0] if ':' in qrcode else qrcode.split('?')[0] value = float(qrcode.split('?')[1].split('=') [1]) if '?' in qrcode else float( input('Enter Btc/Ltc: ')) elif choice == 'n': address = input('Enter Target Address: ') value = float(input('Enter Bitcoins: ')) else: print('Invalid entry') exit() tx_out[address] = value if address in unused_addresses: unused_addresses.remove(address) if len(tx_out) != out_count: print('Address repetition is not allowed in target') exit() change_address = unused_addresses[0] print('change address: %s' % change_address) return tx_out, change_address def getSourceTargetAddresses(self): if network == 'regtest': self.setUnusedAddressesTest() else: self.setUnusedAddresses() out_count = int(input('Enter Number of Input Addresses: ')) input_addresses = [] for i in range(out_count): address = input('Enter Input Address: ') input_addresses.append(address) out_count = int(input('Enter Number of Target Addresses: ')) out_addresses = [] for i in range(out_count): choice = (input('Scan QR code [Y/n]: ') or 'Y').lower() if choice == 'y': qrcode = qrutils.scanQRCode() if ':' in qrcode and qrcode.split(':')[0] != self.crypto: print( 'Address belong to different cryptocurrency i.e. %s' % qrcode.split(':')[0]) exit() if '?' in qrcode: print( 'QR code should not contain amount. Use option "Create Raw Transaction" instead.' ) exit() address = qrcode.split(':')[1] if ':' in qrcode else qrcode elif choice == 'n': address = input('Enter Target Address: ') else: print('Invalid entry') exit() out_addresses.append(address) if len(set(out_addresses)) != out_count: print('Address repetition is not allowed in target') exit() # print('out_addresses = %s' % out_addresses) return input_addresses, out_addresses def validateAddresses(self): address_valid_map = {} for address in self.jsonobj['Addresses']: address_valid_map[address] = self.rpc_connection.validateaddress( address)['isvalid'] return address_valid_map def setNewAddresses(self, addresses: list): label = self.user label_list = self.rpc_connection.listlabels() existing_addresses = [] if label in label_list: existing_addresses = self.rpc_connection.getaddressesbylabel(label) new_addresses = set(addresses) - set(existing_addresses) # print('new_addresses = %s' % new_addresses) return new_addresses def registerAddresses(self, addresses: list): new_addresses = self.setNewAddresses(addresses) s = [] for address in new_addresses: i = { 'scriptPubKey': { 'address': address }, 'timestamp': 0, 'label': self.user, 'watchonly': True } s.append(i) options = {'rescan': False} if len(s) > 0: self.rpc_connection.importmulti(s, options) return new_addresses def createRawTxn(self, fee_rate): # print('transfer_info_filepath = %s' % self.transfer_info_filepath) sys.stdout.flush() self.registerAddresses(self.jsonobj['Addresses']) raw_txn = create_raw_txn.RawTxn(self.rpc_user, self.rpc_password, self.rpc_port, self.transfer_info_filepath, self.user) txout, change_address = self.getTargetAddresses() self.jsonobj = raw_txn.getRawTxnFromOuts(txout, change_address, fee_rate, self.jsonobj) def createRawTxnToDivideFunds(self, fee_rate): # print('transfer_info_filepath = %s' % self.transfer_info_filepath) sys.stdout.flush() self.registerAddresses(self.jsonobj['Addresses']) raw_txn = create_raw_txn.RawTxn(self.rpc_user, self.rpc_password, self.rpc_port, self.transfer_info_filepath, self.user) input_addresses, out_addresses = self.getSourceTargetAddresses() self.jsonobj = raw_txn.getRawTxnToDivideFunds(input_addresses, out_addresses, fee_rate, self.jsonobj) def publishSignedTxn(self): return self.rpc_connection.sendrawtransaction( self.jsonobj['Signed Txn']) def getFeeRate(self, conf_target_block: float): return self.rpc_connection.estimatesmartfee( conf_target_block)['feerate'] def decodeSignedTransaction(self): return self.rpc_connection.decoderawtransaction( self.jsonobj['Signed Txn'])