mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
Bugfix: Multiple outputs in the same tx are smashed together in one UTXO (#2259)
* have multiple tx items with different vouts in the same tx * pytest added * additions to pytest * account for the funding utxo --------- Co-authored-by: k9ert <k9ert@gmx.de>
This commit is contained in:
parent
c0d657899b
commit
c1413e4f82
2 changed files with 112 additions and 80 deletions
|
|
@ -853,24 +853,73 @@ class Wallet:
|
|||
|
||||
txlist = self._transactions.get_transactions()
|
||||
txlist_dict = {_tx["txid"]: _tx for _tx in txlist}
|
||||
# iterating over the utxos/vouts and creating a list of WalletAwareTxItems
|
||||
for utxo_txid, utxo_vout in {
|
||||
_utxo["txid"]: _utxo["vout"] for _utxo in utxo
|
||||
}.items():
|
||||
# maybe the txlist is outdated and it's a new utxo?!
|
||||
if utxo_txid not in txlist_dict.keys():
|
||||
self.fetch_transactions() # ToDo: make this much slimmer!
|
||||
txlist = self._transactions.get_transactions()
|
||||
txlist_dict = {_tx["txid"]: _tx for _tx in txlist}
|
||||
tx: WalletAwareTxItem = txlist_dict[utxo_txid]
|
||||
# Adding vout, locked and amount
|
||||
tx["vout"] = utxo_vout
|
||||
if tx.txid in locked_utxo_list:
|
||||
tx["locked"] = True
|
||||
|
||||
# Example of utxo
|
||||
# [
|
||||
# {
|
||||
# 'txid': '94725a76db75ea9c955da3c5b2c56f4ef3a9865b68218f79b3a6bed96a8c051c',
|
||||
# 'vout': 1,
|
||||
# 'address': 'bcrt1qycdzldu6vrrgju24kl048w5j65tpxugdp073gy',
|
||||
# ...
|
||||
# },
|
||||
# {
|
||||
# 'txid': '94725a76db75ea9c955da3c5b2c56f4ef3a9865b68218f79b3a6bed96a8c051c',
|
||||
# 'vout': 2,
|
||||
# 'address': 'bcrt1q3ya6hur3cey92fau6tqs099zzpdzn4uzg5ftsd',
|
||||
# ....
|
||||
# },
|
||||
# {
|
||||
# 'txid': 'c185f6fc5cde81c2338d809c6d7e77e789630faee1aae6f2d7defe9215405b3b',
|
||||
# 'vout': 0,
|
||||
# 'address': 'bcrt1q5vfwlldskej3u9dnrjpj6cj7rrq8lxkvkpgw5h',
|
||||
# ...
|
||||
# }
|
||||
# ]
|
||||
|
||||
# Example of utxo_dict:
|
||||
# {
|
||||
# '94725a76db75ea9c955da3c5b2c56f4ef3a9865b68218f79b3a6bed96a8c051c': [1, 2],
|
||||
# 'c185f6fc5cde81c2338d809c6d7e77e789630faee1aae6f2d7defe9215405b3b': [0],
|
||||
# 'b27b4e849568024881a9a81618bdab8da40bde9ea468f582f2c1861ddf125caf': [1]
|
||||
# }
|
||||
|
||||
utxo_dict = {}
|
||||
for _utxo in utxo:
|
||||
if _utxo["txid"] not in utxo_dict:
|
||||
utxo_dict[_utxo["txid"]] = [_utxo["vout"]]
|
||||
else:
|
||||
tx["locked"] = False
|
||||
tx["amount"] = tx.utxo_amount
|
||||
_full_utxo.append(tx)
|
||||
utxo_dict[_utxo["txid"]].append(_utxo["vout"])
|
||||
|
||||
# Iterating over utxo_dict to create a list of WalletAwareTxItems
|
||||
for utxo_txid, utxo_vouts in utxo_dict.items():
|
||||
for utxo_vout in utxo_vouts:
|
||||
# maybe the txlist is outdated and it's a new utxo?!
|
||||
if utxo_txid not in txlist_dict.keys():
|
||||
self.fetch_transactions() # ToDo: make this much slimmer!
|
||||
txlist = self._transactions.get_transactions()
|
||||
txlist_dict = {_tx["txid"]: _tx for _tx in txlist}
|
||||
tx: WalletAwareTxItem = txlist_dict[utxo_txid]
|
||||
# Create a copy of the tx item for each vout
|
||||
tx_copy = tx.copy()
|
||||
# Adding vout, locked and amount to the copy
|
||||
tx_copy["vout"] = utxo_vout
|
||||
if tx.txid in locked_utxo_list:
|
||||
tx_copy["locked"] = True
|
||||
else:
|
||||
tx_copy["locked"] = False
|
||||
if len(utxo_vouts) < 2:
|
||||
tx_copy["amount"] = tx.utxo_amount
|
||||
else:
|
||||
for utxo_item in utxo:
|
||||
if (
|
||||
utxo_item["txid"] == utxo_txid
|
||||
and utxo_item["vout"] == utxo_vout
|
||||
):
|
||||
tx_copy["amount"] = utxo_item["amount"]
|
||||
break
|
||||
|
||||
# Append the copy to the _full_utxo list
|
||||
_full_utxo.append(tx_copy)
|
||||
|
||||
# Finally sorting:
|
||||
self._full_utxo = sorted(
|
||||
|
|
@ -883,67 +932,6 @@ class Wallet:
|
|||
self._full_utxo = []
|
||||
raise SpecterError(f"Failed to load utxos, {type(e).__name__}: {e}")
|
||||
|
||||
def check_utxo_orig(self):
|
||||
"""fetches the utxo-set from core and stores the result in self.__full_utxo which is
|
||||
a List[WalletAwareTxItem] enriched with utxo specific data:
|
||||
* item["locked"] if the item is locked in Core
|
||||
* item["vout"] to enable its use in coinselection
|
||||
* item["amount"] is the utxo_amount
|
||||
"""
|
||||
result_utxos = []
|
||||
try:
|
||||
# listunspent only lists not locked utxos
|
||||
# so we need to unlock, then list, then lock back
|
||||
locked_utxo = self.rpc.listlockunspent()
|
||||
# e.g. [
|
||||
# {'txid': '1211aaba2b261e1bf06a3d46d5ac8837cee4669980ddfa7e1e73b2a7cd593f23', 'vout': 0},
|
||||
# {'txid': '7cdc6c668b665c47de5823caca41c194f2d70815420c564aa4fa5786d4c0693f', 'vout': 0}
|
||||
# ]
|
||||
if locked_utxo:
|
||||
self.rpc.lockunspent(True, locked_utxo)
|
||||
utxo = self.rpc.listunspent(0)
|
||||
utxo_dict = {item["txid"]: item for item in utxo}
|
||||
if locked_utxo:
|
||||
self.rpc.lockunspent(False, locked_utxo)
|
||||
for tx in utxo:
|
||||
if [
|
||||
_tx
|
||||
for _tx in locked_utxo
|
||||
if _tx["txid"] == tx["txid"] and _tx["vout"] == tx["vout"]
|
||||
]:
|
||||
tx["locked"] = True
|
||||
# list only the ones we know (have descriptor for it)
|
||||
utxo = [tx for tx in utxo if tx.get("desc", "")]
|
||||
# We need a list in order to check later whether an txid is in that list
|
||||
utxo = [tx["txid"] for tx in utxo]
|
||||
# same for locked_utxo_list
|
||||
locked_utxo_list = [tx["txid"] for tx in locked_utxo]
|
||||
|
||||
# This is the full txlist:
|
||||
txlist = self._transactions.get_transactions()
|
||||
tx: WalletAwareTxItem
|
||||
for tx in txlist:
|
||||
|
||||
if tx.txid in utxo:
|
||||
result_utxos.append(tx)
|
||||
# ToDo: What if a tx contains more than one spendable output?
|
||||
tx["vout"] = utxo_dict[tx.txid]["vout"]
|
||||
if tx.txid in locked_utxo_list:
|
||||
tx["locked"] = True
|
||||
else:
|
||||
tx["locked"] = False
|
||||
tx["amount"] = tx.utxo_amount
|
||||
|
||||
self._full_utxo = sorted(
|
||||
result_utxos,
|
||||
key=lambda result_utxos: result_utxos["time"],
|
||||
reverse=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
self._full_utxo = []
|
||||
raise SpecterError(f"Failed to load utxos, {e}")
|
||||
|
||||
def getdata(self):
|
||||
self.fetch_transactions()
|
||||
self.check_utxo()
|
||||
|
|
@ -973,8 +961,8 @@ class Wallet:
|
|||
@property
|
||||
def full_utxo(self) -> List[WalletAwareTxItem]:
|
||||
"""Lazy getter for the current full_utxo-set. Full means locked and not locked utxo. The result
|
||||
us a List of WalletAwareTxItem. Check check_utxo for more details
|
||||
Call check_utxo() to update it with recent data from core
|
||||
is a list of WalletAwareTxItem. Call check_utxo() to update it with recent data from Bitcoin Core.
|
||||
For more details, see check_utxo().
|
||||
"""
|
||||
if hasattr(self, "_full_utxo"):
|
||||
return self._full_utxo
|
||||
|
|
|
|||
|
|
@ -304,3 +304,47 @@ def test_check_utxo_and_amounts(funded_hot_wallet_1: Wallet):
|
|||
assert wallet.amount_locked_unsigned == selected_coins_amount_sum
|
||||
wallet.delete_pending_psbt(psbt.to_dict()["tx"]["txid"])
|
||||
assert wallet.amount_locked_unsigned == 0
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_multiple_outputs_in_one_tx(
|
||||
funded_ghost_machine_wallet: Wallet,
|
||||
funded_hot_wallet_1: Wallet,
|
||||
hot_wallet_device_1,
|
||||
):
|
||||
receiving_wallet = funded_ghost_machine_wallet
|
||||
spending_wallet = funded_hot_wallet_1
|
||||
# Ghost machine addresses (high address index)
|
||||
address_list = [
|
||||
"bcrt1qfhxsdnrquh63g7u9f25mmucsavsszz6hqym2rs",
|
||||
"bcrt1qm2hl7qqaz7ap9279vphdrxey97005wx7rm7k7n",
|
||||
"bcrt1qd5prsvv2kktulyc4a4gj0y6frszmux0wkzafxu",
|
||||
]
|
||||
|
||||
# Create a PSBT with those three addresses, convert it to a transaction and send this transaction
|
||||
psbt = spending_wallet.createpsbt(address_list, [1, 2, 3], False, 0, 1)
|
||||
signed_psbt = hot_wallet_device_1.sign_psbt(psbt.to_string(), spending_wallet)
|
||||
transaction = spending_wallet.rpc.finalizepsbt(signed_psbt["psbt"])
|
||||
transaction_hex = transaction["hex"]
|
||||
spending_wallet.rpc.sendrawtransaction(transaction_hex)
|
||||
|
||||
# Check the full utxo list of the receiving wallet
|
||||
receiving_wallet.check_utxo()
|
||||
full_utxo = receiving_wallet.full_utxo
|
||||
assert len(full_utxo) == 4 # The wallet already has one UTXO from being funded
|
||||
assert full_utxo[0]["amount"] == 1
|
||||
assert full_utxo[1]["amount"] == 2
|
||||
assert full_utxo[2]["amount"] == 3
|
||||
assert full_utxo[3]["amount"] == 20 # The funding UTXO
|
||||
|
||||
# Confirming doesn't change anything since check_utxo() calls listunspent with 0 blocks as minconf argument
|
||||
random_address = "bcrt1q7mlxxdna2e2ufzgalgp5zhtnndl7qddlxjy5eg" # Does not belong to the ghost machine wallet
|
||||
spending_wallet.rpc.generatetoaddress(
|
||||
1, random_address
|
||||
) # We don't need the confirmation,
|
||||
receiving_wallet.check_utxo()
|
||||
assert len(full_utxo) == 4
|
||||
assert full_utxo[0]["amount"] == 1
|
||||
assert full_utxo[1]["amount"] == 2
|
||||
assert full_utxo[2]["amount"] == 3
|
||||
assert full_utxo[3]["amount"] == 20
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue