Bugfix: TXs in csv and in the UI now get blocktime for time if confirmed fixes #1552 (#1559)

* initial blocktime (tests)

* regtest tx

* json loads

* bit more testing

* complete txlist-test

* internediate commit

* Feature: prevent_mining for bitcoind

* speedup ctrl-c

* proper handling of len() == 0

* having automatic invalidation in the txlist cache

* fix cypress automatic build

* kick

* blocktime vs. mempooltime in UI

* fix blocktime

* correct time for including historical pricedata

* time is not a good var name

* remove print-statements

* work on tests

* finally fix the test
This commit is contained in:
Kim Neunert 2022-02-24 14:23:25 +01:00 committed by GitHub
parent dad97f55ff
commit e33a178a21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 442 additions and 36 deletions

View file

@ -7,19 +7,20 @@ import signal
import sys
import time
from pathlib import Path
from threading import Event
import click
import psutil
from flask import Config
from ..config import DEFAULT_CONFIG
from ..process_controller.node_controller import find_node_executable
from ..process_controller.elementsd_controller import ElementsPlainController
from ..process_controller.node_controller import find_node_executable
from .utils import (
Echo,
compute_data_dir_and_set_config_obj,
kill_node_process,
purge_node_data_dir,
compute_data_dir_and_set_config_obj,
)
logger = logging.getLogger(__name__)
@ -385,29 +386,46 @@ def miner_loop(node_impl, my_node, data_folder, mining_every_x_seconds, echo):
f"height: {my_node.rpcconn.get_rpc().getblockchaininfo()['blocks']} | ",
nl=False,
)
exit = Event()
def exit_now(signo, _frame):
exit.set()
for sig in ("HUP", "INT"):
signal.signal(getattr(signal, "SIG" + sig), exit_now)
prevent_mining_file = Path("prevent_mining")
i = 0
while True:
try:
my_node.mine()
current_height = my_node.rpcconn.get_rpc().getblockchaininfo()["blocks"]
exit.wait(mining_every_x_seconds)
if not prevent_mining_file.is_file():
my_node.mine()
else:
echo("X", prefix=False, nl=False)
continue
echo("%i" % (i % 10), prefix=False, nl=False)
if i % 10 == 9:
echo(" ", prefix=False, nl=False)
i += 1
if i >= 50:
i = 0
echo("", prefix=False)
echo(
f"height: {current_height} | ",
nl=False,
)
except Exception as e:
logger.debug(
f"Caught {e}, Couldn't mine, assume SIGTERM occured => exiting!"
)
echo(f"THE_END(@height:{current_height})")
if prevent_mining_file.is_file():
echo("Deleting file prevent_mining")
prevent_mining_file.unlink()
break
echo("%i" % (i % 10), prefix=False, nl=False)
if i % 10 == 9:
echo(" ", prefix=False, nl=False)
i += 1
if i >= 50:
i = 0
echo("", prefix=False)
echo(
f"height: {current_height} | ",
nl=False,
)
time.sleep(mining_every_x_seconds)
def mine_2_specter_wallets(node_impl, my_node, data_folder, echo):

View file

@ -765,14 +765,18 @@ def wallet_overview_utxo_csv():
# Transactions list to user-friendly CSV format
def txlist_to_csv(wallet, _txlist, specter, current_user, includePricesHistory=False):
# Why is this line needed?
# Please remover if you can!
from flask_babel import lazy_gettext as _
txlist = []
for tx in _txlist:
if isinstance(tx["address"], list):
_tx = tx.copy()
tx = tx.copy()
for i in range(0, len(tx["address"])):
_tx["address"] = tx["address"][i]
_tx["amount"] = tx["amount"][i]
txlist.append(_tx.copy())
tx["address"] = tx["address"][i]
tx["amount"] = tx["amount"][i]
txlist.append(tx.copy())
else:
txlist.append(tx.copy())
data = StringIO()
@ -825,34 +829,38 @@ def txlist_to_csv(wallet, _txlist, specter, current_user, includePricesHistory=F
if specter.unit == "sat":
value = float(tx["amount"])
tx["amount"] = round(value * 1e8)
amount_price = "not supported"
rate = "not supported"
if tx["blocktime"]:
timestamp = tx["blocktime"]
else:
timestamp = tx["time"]
if includePricesHistory:
try:
rate, _ = get_price_at(specter, current_user, timestamp=tx["time"])
rate, _ = get_price_at(specter, current_user, timestamp)
rate = float(rate)
if specter.unit == "sat":
rate = rate / 1e8
amount_price = float(tx["amount"]) * rate
amount_price = round(amount_price * 100) / 100
if specter.unit == "sat":
rate = round(1 / rate)
except SpecterError as se:
logger.error(se)
success = False
amount_price = None
rate = "-"
row = (
time.strftime("%Y-%m-%d", time.localtime(tx["time"])),
time.strftime("%Y-%m-%d", time.localtime(timestamp)),
label,
tx["category"],
round(tx["amount"], (0 if specter.unit == "sat" else 8)),
round(amount_price * 100) / 100
if amount_price is not None
else "no-support",
amount_price,
rate,
tx["txid"],
tx["address"],
tx["blockheight"],
tx["time"],
time,
)
if not wallet:
row = (tx.get("wallet_alias", ""),) + row
@ -954,6 +962,7 @@ def wallet_addresses_list_to_csv(addresses_list):
def process_txlist(txlist, idx=0, limit=100, search=None, sortby=None, sortdir="asc"):
"""Prepares the txlist for the ui filtering it with the search-criterias and sorting it"""
if search:
search_lower = search.lower()
txlist = [

View file

@ -246,7 +246,18 @@
}
// Set time
this.time.innerText = this.hideSensitiveInfo ? '###########' : (new Date(this.tx.time * 1000)).toLocaleString()
if (this.hideSensitiveInfo) {
this.time.innerText = '###########'
} else {
// blocktime for confirmed Txs
if (this.tx.blocktime) {
this.time.innerText = (new Date(this.tx.blocktime * 1000)).toLocaleString()
} else {
this.time.innerText = (new Date(this.tx.time * 1000)).toLocaleString()
}
}
// Show blockhash
if (this.showBlockhash) {

View file

@ -3,7 +3,8 @@ Manages the list of transactions for the wallet
"""
from typing import Union
import os
from .persistence import write_csv, read_csv
from .specter_error import SpecterError
from .persistence import delete_file, write_csv, read_csv
from .helpers import get_address_from_dict
from embit.transaction import Transaction
from embit.liquid.networks import get_network
@ -47,6 +48,7 @@ class TxItem(dict, AbstractTxListContext):
"blockhash", # str, blockhash, None if not confirmed
"blockheight", # int, blockheight, None if not confirmed
"time", # int (timestamp in seconds), time received
"blocktime", # int (timestamp in seconds), time the block was mined
"bip125-replaceable", # str ("yes" / "no"), whatever RBF is enabled for the transaction
"conflicts", # rbf conflicts, list of txids
"vsize",
@ -60,6 +62,7 @@ class TxItem(dict, AbstractTxListContext):
str,
int,
int,
int,
str,
parse_arr,
int,
@ -180,6 +183,7 @@ class TxItem(dict, AbstractTxListContext):
"blockhash": self["blockhash"],
"blockheight": self["blockheight"],
"time": self["time"],
"blocktime": self["blocktime"],
"conflicts": self["conflicts"],
"bip125-replaceable": self["bip125-replaceable"],
"vsize": self["vsize"],
@ -218,7 +222,7 @@ class TxList(dict, AbstractTxListContext):
logger.error(e)
self._file_exists = file_exists
def save(self):
def _save(self):
# check if we have at least one transaction
if self:
# Dump all transactions to binary files
@ -226,7 +230,10 @@ class TxList(dict, AbstractTxListContext):
for tx in self.values():
tx.dump()
write_csv(self.path, list(self.values()), self.ItemCls)
self._file_exists = True
self._file_exists = True
else:
delete_file(self.path)
self._file_exists = False
def getfetch(self, txid):
"""
@ -249,7 +256,10 @@ class TxList(dict, AbstractTxListContext):
decode=True will decode transaction similar to Core's decoderawtransaction
"""
# if we don't know blockheigth or transaction
# we get it from rpc
# we invalidate which results in asking core
if txid in self and self[txid]["blockheight"] == None:
self.invalidate(txid)
if blockheight is None or txid not in self:
tx = self.rpc.gettransaction(txid)
if "time" not in tx:
@ -263,10 +273,17 @@ class TxList(dict, AbstractTxListContext):
if full:
res["hex"] = tx.hex
if decode:
res.update(self.decoderawtransaction(tx.hex))
res.update(self._decoderawtransaction(tx.hex))
return res
def decoderawtransaction(self, tx: Union[Transaction, str, bytes]):
def invalidate(self, txid):
"""removes a tx from the list"""
if txid not in self:
raise SpecterError(f"TX with txid {txid} does not exit in {self}")
del self[txid]
self._save()
def _decoderawtransaction(self, tx: Union[Transaction, str, bytes]):
return SpecterTx(self, tx).to_dict()
def add(self, txs):
@ -280,6 +297,7 @@ class TxList(dict, AbstractTxListContext):
"blockheight", - int blockheight if confirmed, None otherwise
"blockhash", - str blockhash if confirmed, None otherwise
"time", - int unix timestamp in seconds when tx was received
"blocktime", - int unix timestamp in seconds the block was mined
"conflicts", - list of txids spending the same inputs (rbf)
"bip125-replaceable", - str ("yes" or "no") - is rbf enabled for this tx
}
@ -302,6 +320,7 @@ class TxList(dict, AbstractTxListContext):
"blockheight": tx.get("blockheight", None),
"blockhash": tx.get("blockhash", None),
"time": time,
"blocktime": tx.get("blocktime", None),
"conflicts": tx.get("walletconflicts", []),
"bip125-replaceable": tx.get("bip125-replaceable", "no"),
"hex": tx.get("hex", None),
@ -319,8 +338,8 @@ class TxList(dict, AbstractTxListContext):
self._addresses.set_used(addresses)
# detect category, amounts and addresses
for tx in [self[txid] for txid in self if txid in txs]:
self.fill_missing(tx)
self.save()
self._fill_missing(tx)
self._save()
def _update_destinations(self, tx, outs):
addresses = [out.get("address", "Unknown") for out in outs]
@ -339,7 +358,12 @@ class TxList(dict, AbstractTxListContext):
psbt.update(updated)
return psbt
def fill_missing(self, tx):
def _fill_missing(self, tx):
"""This seem to calculate the category of the tx which is one of:
mixed (default), generate, selftransfer, receive or send
Also the tx gets a key with a boolean to figure out whether its "mine"
"""
raw_tx = tx.tx
psbt = self._get_psbt(raw_tx)
# detect category

191
tests/test_txlist.py Normal file
View file

@ -0,0 +1,191 @@
import json
import os
import time
from binascii import hexlify
from datetime import datetime
from pathlib import Path
from cryptoadvance.specter.process_controller.bitcoind_controller import (
BitcoindPlainController,
)
from cryptoadvance.specter.txlist import TxItem, TxList
from embit.descriptor.arguments import Key
from embit.descriptor.descriptor import Descriptor
from embit.transaction import Transaction, TransactionInput
from mock import MagicMock
descriptor = "pkh([78738c82/84h/1h/0h]vpub5YN2RvKrA9vGAoAdpsruQGfQMWZzaGt3M5SGMMhW8i2W4SyNSHMoLtyyLLS6EjSzLfrQcbtWdQcwNS6AkCWne1Y7U8bt9JgVYxfeH9mCVPH/1/*)"
# The example transaction from a regtest
# 42f5c9e826e52cde883cde7a6c7b768db302e0b8b32fc52db75ad3c5711b4a9e
with open("tests/xtestdata_txlist/tx1_confirmed.json") as f:
tx1_confirmed = json.load(f)
#
with open("tests/xtestdata_txlist/tx2_unconfirmed.json") as f:
tx2_unconfirmed = json.load(f)
with open("tests/xtestdata_txlist/tx2_confirmed.json") as f:
tx2_confirmed = json.load(f)
with open("tests/xtestdata_txlist/tx2_confirmed2.json") as f:
tx2_confirmed2 = json.load(f)
def test_understandTransaction():
mytx = Transaction.from_string(tx1_confirmed["hex"])
assert mytx.version == 2
assert mytx.locktime == 415
assert type(mytx.vin[0]) == TransactionInput
assert (
hexlify(mytx.vin[0].txid)
== b"c7c9dd852fa9cbe72b2f6e3b2eeba1a2b47dc4422b3719a55381be8010d7993f"
)
assert mytx.vout[0].value == 1999999890
assert (
hexlify(mytx.txid())
== b"42f5c9e826e52cde883cde7a6c7b768db302e0b8b32fc52db75ad3c5711b4a9e"
)
def test_TxItem(empty_data_folder):
mytxitem = TxItem(
None,
[],
empty_data_folder,
hex=tx1_confirmed["hex"],
blocktime=1642182445, # arbitrary stuff can get passed
)
# a TxItem pretty much works like a hash with some extrafunctionality
assert mytxitem["blocktime"] == 1642182445
# We can also add data after the fact
mytxitem["confirmations"] = 123
assert mytxitem.tx.vout
assert mytxitem["confirmations"] == 123
def test_txlist(empty_data_folder, bitcoin_regtest):
parent_mock = MagicMock()
bitcoin_regtest.get_rpc().createwallet("txlist1")
wrpc = bitcoin_regtest.get_rpc().wallet("txlist1")
parent_mock.rpc = wrpc
parent_mock.descriptor = Descriptor.from_string(descriptor)
assert type(parent_mock.descriptor.key) == Key
assert parent_mock.descriptor.key.allowed_derivation != None
assert parent_mock.descriptor.to_string() == descriptor
filename = os.path.join(empty_data_folder, "my_filename.csv")
mytxlist = TxList(filename, parent_mock, MagicMock())
# mytxlist.descriptor = descriptor
mytxlist.add({tx1_confirmed["txid"]: tx1_confirmed})
# .add will save implicitely.
# mytxlist._save()
with open(filename, "r+") as file:
# Reading form a file
assert file.readline().startswith(
"txid,blockhash,blockheight,time,blocktime,bip125-replaceable,conflicts,vsize,category,address,amount,ismine"
)
assert file.readline().startswith(
"42f5c9e826e52cde883cde7a6c7b768db302e0b8b32fc52db75ad3c5711b4a9e,72523c637e0b93505806564495b1acf915a88bacc45f50e35e8a536becd2f914,,1642494258,1642494258,no,[],,receive,Unknown,19.9999989,False"
)
assert len(mytxlist) == 1
mytxlist.invalidate(tx1_confirmed["txid"])
assert len(mytxlist) == 0
assert not Path(filename).is_file()
# Mock rpc-calls
mock_rpc = MagicMock()
mock_rpc.gettransaction.return_value = tx2_confirmed
mock_parent = MagicMock()
mock_parent.rpc = mock_rpc
mytxlist.parent = mock_parent
# mytxlist.getfetch("42f5c9e826e52cde883cde7a6c7b768db302e0b8b32fc52db75ad3c5711b4a9e")
# assert False
def test_txlist_invalidate(empty_data_folder, bitcoin_regtest: BitcoindPlainController):
def print_time():
curr_dt = datetime.now()
print(f"current time\t\t\t\t\t: {int(round(curr_dt.timestamp()))}\n")
# Setup the infra
rpc = bitcoin_regtest.get_rpc()
rpc.mine
rpc.createwallet("txlist2")
wrpc = rpc.wallet("txlist2")
address = wrpc.getnewaddress()
bitcoin_regtest.testcoin_faucet(address, amount=10, confirm_payment=True)
# itcoin_regtest.mine(self, address=address, block_count=100)
# Create the mytxlist
parent_mock = MagicMock()
parent_mock.rpc = wrpc
parent_mock.descriptor = Descriptor.from_string(descriptor)
assert type(parent_mock.descriptor.key) == Key
assert parent_mock.descriptor.key.allowed_derivation != None
assert parent_mock.descriptor.to_string() == descriptor
filename = os.path.join(empty_data_folder, "my_filename.csv")
mytxlist = TxList(filename, parent_mock, MagicMock())
# create a tx
txid = wrpc.sendtoaddress("bcrt1qsj30deg0fgzckvlrn5757yk55yajqv6dqx0x7u", "1")
tx_via_core = wrpc.gettransaction(txid)
print_time()
assert tx_via_core, "we have created a transaction"
assert tx_via_core["confirmations"] == 0, "it's not confirmed, yet"
print(f"TxId: {txid}")
print(f"Tx via core:\n{tx_via_core}\n")
assert (
tx_via_core["time"] == tx_via_core["timereceived"]
), "time and timereceived should be the same"
print(f"timereceived \t\t\t\t\t: {tx_via_core['time']}")
print_time()
tx_item = mytxlist.getfetch(txid)
assert isinstance(tx_item, TxItem), "The tx should get returned via the txlist"
assert (
mytxlist[txid] == tx_item
), "is should be the same item you get via obtaining it via the dict-method"
# analyzing the content of the tx
tx = mytxlist.gettransaction(txid)
print(f"Tx via cache:\n{tx}\n")
mpool_hittime = tx["time"]
assert tx["blockheight"] == None
print(f"this is the time when the tx hit the mempool {mpool_hittime}")
print_time()
print("let's sleep for 2 seconds")
time.sleep(2)
print_time()
# Get the tx confirmed
bitcoin_regtest.mine(block_count=1)
print("\n-----------------------mining----------------------------------\n")
tx_via_core = wrpc.gettransaction(txid)
assert tx_via_core["confirmations"] == 1, "it should now be broadcasted"
print(f"Tx via core:\n{tx_via_core}\n")
print(f"blocktime \t\t\t\t\t: {tx_via_core['blocktime']}")
print_time()
print(
f"Difference of blocktime - current-time:\t\t\t {int(round(datetime.now().timestamp())) - tx_via_core['blocktime']}"
)
print(f"How is it possible that this number is negative?")
assert (
tx_via_core["blocktime"] > tx_via_core["time"]
), "blocktime should be larger than time"
# optional: invalidate
# mytxlist.invalidate(txid)
# get it again
tx = mytxlist.gettransaction(txid)
print(f"Tx via cache:\n{tx}\n")
assert tx["blockheight"], "The tx should now have a blockheight"
assert tx["blocktime"], "The tx should now have a blocktime"
assert (
tx["blocktime"] > mpool_hittime
), "The time of the transaction should now be bigger than the time it got hit the mempool"

54
tests/test_wallet.py Normal file
View file

@ -0,0 +1,54 @@
import json, logging, pytest, time, os
from cryptoadvance.specter.specter import Specter
from cryptoadvance.specter.managers.wallet_manager import WalletManager
from cryptoadvance.specter.wallet import Wallet
from conftest import instantiate_bitcoind_controller
logger = logging.getLogger(__name__)
def test_import_address_labels(caplog, specter_regtest_configured):
caplog.set_level(logging.INFO)
specter = specter_regtest_configured
# Create a new device that can sign psbts (Bitcoin Core hot wallet)
device = specter.device_manager.add_device(
name="bitcoin_core_hot_wallet", device_type="bitcoincore", keys=[]
)
device.setup_device(file_password=None, wallet_manager=specter.wallet_manager)
device.add_hot_wallet_keys(
mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
passphrase="",
paths=["m/49h/0h/0h"],
file_password=None,
wallet_manager=specter.wallet_manager,
testnet=True,
keys_range=[0, 1000],
keys_purposes=[],
)
wallet: Wallet = specter.wallet_manager.create_wallet(
"bitcoincore_test_wallet", 1, "sh-wpkh", [device.keys[0]], [device]
)
# Fund the wallet. Going to need a LOT of utxos to play with.
logger.info("Generating utxos to wallet")
test_address = wallet.getnewaddress() # 2NCSZrX49HHyzUy6oj8ggm9WD19hFvjzzou
wallet.rpc.generatetoaddress(1, test_address)[0]
# newly minted coins need 100 blocks to get spendable
# let's mine another 100 blocks to get these coins spendable
trash_address = wallet.getnewaddress()
wallet.rpc.generatetoaddress(100, trash_address)
# the utxo is only available after the 100 mined blocks
utxos = wallet.rpc.listunspent()
# txid of the funding of test_address
txid = utxos[0]["txid"]
assert wallet._addresses[test_address]["label"] is None
number_of_addresses = len(wallet._addresses)
assert wallet.txlist()[0]["blockheight"] != None
assert wallet.txlist()[0]["blocktime"] != None

View file

@ -0,0 +1,45 @@
{
"in_active_chain": true,
"txid": "42f5c9e826e52cde883cde7a6c7b768db302e0b8b32fc52db75ad3c5711b4a9e",
"hash": "d93c51fe9b7d001a795e98dd1357de42647cda08fe5072703f7e438154402bb6",
"version": 2,
"size": 191,
"vsize": 110,
"weight": 437,
"locktime": 415,
"vin": [
{
"txid": "c7c9dd852fa9cbe72b2f6e3b2eeba1a2b47dc4422b3719a55381be8010d7993f",
"vout": 0,
"scriptSig": {
"asm": "",
"hex": ""
},
"txinwitness": [
"304402201088bfd110dd891b7f16c5d50d10e6f99cf79d48673d890e3563f8275500315b02205a75e89f794a3e681fe6a7622c642323ac3c802b5f60da2547b2589982795a2401",
"02578993563d2d00cf1047011fe77a07181a1ab0467044d9b12857c3df65653a50"
],
"sequence": 4294967294
}
],
"vout": [
{
"value": 19.99999890,
"n": 0,
"scriptPubKey": {
"asm": "0 84a2f6e50f4a058b33e39d3d4f12d4a13b20334d",
"hex": "001484a2f6e50f4a058b33e39d3d4f12d4a13b20334d",
"reqSigs": 1,
"type": "witness_v0_keyhash",
"addresses": [
"bcrt1qsj30deg0fgzckvlrn5757yk55yajqv6dqx0x7u"
]
}
}
],
"hex": "020000000001013f99d71080be8153a519372b42c47db4a2a1eb2e3b6e2f2be7cba92f85ddc9c70000000000feffffff01929335770000000016001484a2f6e50f4a058b33e39d3d4f12d4a13b20334d0247304402201088bfd110dd891b7f16c5d50d10e6f99cf79d48673d890e3563f8275500315b02205a75e89f794a3e681fe6a7622c642323ac3c802b5f60da2547b2589982795a24012102578993563d2d00cf1047011fe77a07181a1ab0467044d9b12857c3df65653a509f010000",
"blockhash": "72523c637e0b93505806564495b1acf915a88bacc45f50e35e8a536becd2f914",
"confirmations": 222,
"time": 1642494258,
"blocktime": 1642494258
}

View file

@ -0,0 +1,18 @@
{
"amount": 0.00000000,
"fee": -0.00000141,
"confirmations": 1,
"blockhash": "14859527b8638b2d051dd9541f750abf6ae9d973289c9d8eca3f3c04ddbbffc2",
"blockheight": 2271,
"blockindex": 1,
"blocktime": 1645012450,
"txid": "9dbbce06fed2f949660240afce2bb18414ea647952681fd1021693fb2d7e30db",
"walletconflicts": [
],
"time": 1645012282,
"timereceived": 1645012282,
"bip125-replaceable": "no",
"details": [
],
"hex": "02000000000101d4103f45654a10290d2f735fa78ec9368bbcc5ab1fd010b82e54c1ec00aad13f0100000000fdffffff0273b23f7100000000160014c04bf2046eb41344c5e2b5b7b2d26a5ed0cbdde100e1f505000000001600143f2593534164f23051edce61035a5b135bea69390247304402201fda244457566606106f8e36efb0a6f041c4ed84c82fb6823ad680d7ef8efacf022069978308a7f1fb65e213fa4f4294ab18533fbbe5194a2c192ee718aef9a7cb2c0121024f0c2c509e22e8d934541315f6bf816c6ca72dc19bf8aa50e02faa37aad0ff46dd080000"
}

View file

@ -0,0 +1,18 @@
{
"amount": 0.00000000,
"fee": -0.00000141,
"confirmations": 5,
"blockhash": "14859527b8638b2d051dd9541f750abf6ae9d973289c9d8eca3f3c04ddbbffc2",
"blockheight": 2271,
"blockindex": 1,
"blocktime": 1645012450,
"txid": "9dbbce06fed2f949660240afce2bb18414ea647952681fd1021693fb2d7e30db",
"walletconflicts": [
],
"time": 1645012282,
"timereceived": 1645012282,
"bip125-replaceable": "no",
"details": [
],
"hex": "02000000000101d4103f45654a10290d2f735fa78ec9368bbcc5ab1fd010b82e54c1ec00aad13f0100000000fdffffff0273b23f7100000000160014c04bf2046eb41344c5e2b5b7b2d26a5ed0cbdde100e1f505000000001600143f2593534164f23051edce61035a5b135bea69390247304402201fda244457566606106f8e36efb0a6f041c4ed84c82fb6823ad680d7ef8efacf022069978308a7f1fb65e213fa4f4294ab18533fbbe5194a2c192ee718aef9a7cb2c0121024f0c2c509e22e8d934541315f6bf816c6ca72dc19bf8aa50e02faa37aad0ff46dd080000"
}

View file

@ -0,0 +1,18 @@
{
"amount": 0.00000000,
"fee": -0.00000141,
"confirmations": 1,
"blockhash": "14859527b8638b2d051dd9541f750abf6ae9d973289c9d8eca3f3c04ddbbffc2",
"blockheight": 2271,
"blockindex": 1,
"blocktime": 1645012450,
"txid": "9dbbce06fed2f949660240afce2bb18414ea647952681fd1021693fb2d7e30db",
"walletconflicts": [
],
"time": 1645012282,
"timereceived": 1645012282,
"bip125-replaceable": "no",
"details": [
],
"hex": "02000000000101d4103f45654a10290d2f735fa78ec9368bbcc5ab1fd010b82e54c1ec00aad13f0100000000fdffffff0273b23f7100000000160014c04bf2046eb41344c5e2b5b7b2d26a5ed0cbdde100e1f505000000001600143f2593534164f23051edce61035a5b135bea69390247304402201fda244457566606106f8e36efb0a6f041c4ed84c82fb6823ad680d7ef8efacf022069978308a7f1fb65e213fa4f4294ab18533fbbe5194a2c192ee718aef9a7cb2c0121024f0c2c509e22e8d934541315f6bf816c6ca72dc19bf8aa50e02faa37aad0ff46dd080000"
}