Host-customized fork of https://github.com/tecnovert/basicswap/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

700 lines
25 KiB

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import base64
import hashlib
import logging
import random
from basicswap.basicswap_util import (
getVoutByScriptPubKey,
TxLockTypes
)
from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.messages import (
uint256_from_str,
)
from basicswap.interface.btc import Secp256k1Interface
from basicswap.util import (
ensure,
)
from basicswap.util.address import (
b58decode,
b58encode,
)
from basicswap.util.crypto import (
blake256,
hash160,
ripemd160,
)
from basicswap.util.script import (
SerialiseNumCompact,
)
from basicswap.util.extkey import ExtKeyPair
from basicswap.util.integer import encode_varint
8 months ago
from basicswap.interface.dcr.rpc import make_rpc_func
from .messages import (
CTransaction,
CTxIn,
CTxOut,
COutPoint,
SigHashType,
TxSerializeType,
)
from .script import (
push_script_data,
OP_HASH160,
OP_EQUAL,
OP_DUP,
OP_EQUALVERIFY,
OP_CHECKSIG,
)
from coincurve.keys import (
PrivateKey,
PublicKey,
)
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22)
SEQUENCE_LOCKTIME_MASK = 0x0000f
SigHashSerializePrefix: int = 1
SigHashSerializeWitness: int = 3
def DCRSignatureHash(sign_script: bytes, hash_type: SigHashType, tx: CTransaction, idx: int) -> bytes:
masked_hash_type = hash_type & SigHashType.SigHashMask
if masked_hash_type != SigHashType.SigHashAll:
raise ValueError('todo')
# Prefix hash
sign_tx_in_idx: int = idx
sign_vins = tx.vin
if hash_type & SigHashType.SigHashAnyOneCanPay != 0:
sign_vins = [tx.vin[idx],]
sign_tx_in_idx = 0
hash_buffer = bytearray()
version: int = tx.version | (SigHashSerializePrefix << 16)
hash_buffer += version.to_bytes(4, 'little')
hash_buffer += encode_varint(len(sign_vins))
for txi_n, txi in enumerate(sign_vins):
hash_buffer += txi.prevout.hash.to_bytes(32, 'little')
hash_buffer += txi.prevout.n.to_bytes(4, 'little')
hash_buffer += txi.prevout.tree.to_bytes(1)
# In the case of SigHashNone and SigHashSingle, commit to 0 for everything that is not the input being signed instead.
if (masked_hash_type == SigHashType.SigHashNone
or masked_hash_type == SigHashType.SigHashSingle) and \
sign_tx_in_idx != txi_n:
hash_buffer += (0).to_bytes(4, 'little')
else:
hash_buffer += txi.sequence.to_bytes(4, 'little')
hash_buffer += encode_varint(len(tx.vout))
for txo_n, txo in enumerate(tx.vout):
if masked_hash_type == SigHashType.SigHashSingle and \
idx != txo_n:
hash_buffer += (-1).to_bytes(8, 'little')
hash_buffer += txo.version.to_bytes(2, 'little')
hash_buffer += encode_varint(0)
continue
hash_buffer += txo.value.to_bytes(8, 'little')
hash_buffer += txo.version.to_bytes(2, 'little')
hash_buffer += encode_varint(len(txo.script_pubkey))
hash_buffer += txo.script_pubkey
hash_buffer += tx.locktime.to_bytes(4, 'little')
hash_buffer += tx.expiry.to_bytes(4, 'little')
prefix_hash = blake256(hash_buffer)
# Witness hash
hash_buffer.clear()
version: int = tx.version | (SigHashSerializeWitness << 16)
hash_buffer += version.to_bytes(4, 'little')
hash_buffer += encode_varint(len(sign_vins))
for txi_n, txi in enumerate(sign_vins):
if sign_tx_in_idx != txi_n:
hash_buffer += encode_varint(0)
continue
hash_buffer += encode_varint(len(sign_script))
hash_buffer += sign_script
witness_hash = blake256(hash_buffer)
hash_buffer.clear()
hash_buffer += hash_type.to_bytes(4, 'little')
hash_buffer += prefix_hash
hash_buffer += witness_hash
return blake256(hash_buffer)
def extract_sig_and_pk(sig_script: bytes) -> (bytes, bytes):
sig = None
pk = None
o: int = 0
num_bytes = sig_script[o]
o += 1
sig = sig_script[o: o + num_bytes]
o += num_bytes
num_bytes = sig_script[o]
o += 1
pk = sig_script[o: o + num_bytes]
return sig, pk
class DCRInterface(Secp256k1Interface):
@staticmethod
def coin_type():
return Coins.DCR
@staticmethod
def exp() -> int:
return 8
@staticmethod
def COIN() -> int:
return 100000000
@staticmethod
def nbk() -> int:
return 32
@staticmethod
def nbK() -> int: # No. of bytes requires to encode a public key
return 33
8 months ago
@staticmethod
def txVersion() -> int:
return 2
@staticmethod
def txoType():
return CTxOut
@staticmethod
def getExpectedSequence(lockType: int, lockVal: int) -> int:
ensure(lockVal >= 1, 'Bad lockVal')
if lockType == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
return lockVal
if lockType == TxLockTypes.SEQUENCE_LOCK_TIME:
secondsLocked = lockVal
# Ensure the locked time is never less than lockVal
if secondsLocked % (1 << SEQUENCE_LOCKTIME_GRANULARITY) != 0:
secondsLocked += (1 << SEQUENCE_LOCKTIME_GRANULARITY)
secondsLocked >>= SEQUENCE_LOCKTIME_GRANULARITY
return secondsLocked | SEQUENCE_LOCKTIME_TYPE_FLAG
raise ValueError('Unknown lock type')
@staticmethod
def watch_blocks_for_scripts() -> bool:
return True
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
8 months ago
self._rpc_host = coin_settings.get('rpchost', '127.0.0.1')
self._rpcport = coin_settings['rpcport']
self._rpcauth = coin_settings['rpcauth']
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
8 months ago
self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
if 'walletrpcport' in coin_settings:
self.rpc_wallet = make_rpc_func(coin_settings['walletrpcport'], self._rpcauth, host=self._rpc_host)
else:
self.rpc_wallet = None
self.blocks_confirmed = coin_settings['blocks_confirmed']
self.setConfTarget(coin_settings['conf_target'])
self._use_segwit = True # Decred is natively segwit
self._connection_type = coin_settings['connection_type']
def use_tx_vsize(self) -> bool:
return False
def pkh(self, pubkey: bytes) -> bytes:
return ripemd160(blake256(pubkey))
def pkh_to_address(self, pkh: bytes) -> str:
prefix = self.chainparams_network()['pubkey_address']
data = prefix.to_bytes(2, 'big') + pkh
checksum = blake256(blake256(data))
return b58encode(data + checksum[0:4])
def sh_to_address(self, sh: bytes) -> str:
assert (len(sh) == 20)
prefix = self.chainparams_network()['script_address']
data = prefix.to_bytes(2, 'big') + sh
checksum = blake256(blake256(data))
return b58encode(data + checksum[0:4])
def decode_address(self, address: str) -> bytes:
addr_data = b58decode(address)
if addr_data is None:
return None
prefixed_data = addr_data[:-4]
checksum = addr_data[-4:]
if blake256(blake256(prefixed_data))[:4] != checksum:
raise ValueError('Checksum mismatch')
return prefixed_data
8 months ago
def testDaemonRPC(self, with_wallet=True) -> None:
if with_wallet:
self.rpc_wallet('getinfo')
8 months ago
else:
self.rpc('getblockchaininfo')
8 months ago
def getChainHeight(self) -> int:
return self.rpc('getblockcount')
def checkWallets(self) -> int:
# Only one wallet possible?
return 1
def initialiseWallet(self, key: bytes) -> None:
# Load with --create
pass
def getDaemonVersion(self):
return self.rpc('getnetworkinfo')['version']
def getBlockchainInfo(self):
return self.rpc('getblockchaininfo')
def getWalletInfo(self):
rv = {}
rv = self.rpc_wallet('getinfo')
wi = self.rpc_wallet('walletinfo')
balances = self.rpc_wallet('getbalance')
default_account_bal = balances['balances'][0] # 0 always default?
rv['balance'] = default_account_bal['spendable']
rv['unconfirmed_balance'] = default_account_bal['unconfirmed']
rv['immature_balance'] = default_account_bal['immaturecoinbaserewards'] + default_account_bal['immaturestakegeneration']
rv['encrypted'] = True
rv['locked'] = True if wi['unlocked'] is False else False
return rv
def getSeedHash(self, seed: bytes) -> bytes:
# m / purpose' / coin_type' / account' / change / address_index
# m/44'/coin_type'/0'/0/0
ek = ExtKeyPair(self.coin_type())
ek.set_seed(seed)
coin_type = self.chainparams_network()['bip44']
ek_purpose = ek.derive(44 | (1 << 31))
ek_coin = ek_purpose.derive(coin_type | (1 << 31))
ek_account = ek_coin.derive(0 | (1 << 31))
return hash160(ek_account.encode_p())
def decodeKey(self, encoded_key: str) -> (int, bytes):
key = b58decode(encoded_key)
checksum = key[-4:]
key = key[:-4]
if blake256(key)[:4] != checksum:
raise ValueError('Checksum mismatch')
return key[2], key[3:]
def encodeKey(self, key_bytes: bytes) -> str:
wif_prefix = self.chainparams_network()['key_prefix']
key_type = 0 # STEcdsaSecp256k1
b = wif_prefix.to_bytes(2, 'big') + key_type.to_bytes(1) + key_bytes
b += blake256(b)[:4]
return b58encode(b)
def loadTx(self, tx_bytes: bytes) -> CTransaction:
tx = CTransaction()
tx.deserialize(tx_bytes)
return tx
def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
tx = self.loadTx(tx_bytes)
sig_hash = DCRSignatureHash(prevout_script, SigHashType.SigHashAll, tx, input_n)
eck = PrivateKey(key_bytes)
return eck.sign(sig_hash, hasher=None) + bytes((SigHashType.SigHashAll,))
def setTxSignatureScript(self, tx_bytes: bytes, script: bytes, txi: int = 0) -> bytes:
tx = self.loadTx(tx_bytes)
tx.vin[txi].signature_script = script
return tx.serialize()
def setTxSignature(self, tx_bytes: bytes, stack, txi: int = 0) -> bytes:
tx = self.loadTx(tx_bytes)
script_data = bytearray()
for data in stack:
push_script_data(script_data, data)
tx.vin[txi].signature_script = script_data
test_ser = tx.serialize()
test_tx = self.loadTx(test_ser)
return tx.serialize()
def stripTxSignature(self, tx_bytes) -> bytes:
tx = self.loadTx(tx_bytes)
return tx.serialize(TxSerializeType.NoWitness)
8 months ago
def getTxSignature(self, tx_hex: str, prevout_data, key_wif: str) -> str:
sig_type, key = self.decodeKey(key_wif)
redeem_script = bytes.fromhex(prevout_data['redeemScript'])
sig = self.signTx(key, bytes.fromhex(tx_hex), 0, redeem_script, self.make_int(prevout_data['amount']))
return sig.hex()
def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool:
tx = self.loadTx(tx_bytes)
sig_hash = DCRSignatureHash(prevout_script, SigHashType.SigHashAll, tx, input_n)
pubkey = PublicKey(K)
return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte
def getTxid(self, tx) -> bytes:
if isinstance(tx, str):
tx = bytes.fromhex(tx)
if isinstance(tx, bytes):
tx = self.loadTx(tx)
return tx.TxHash()
8 months ago
def getScriptDest(self, script: bytes) -> bytes:
# P2SH
script_hash = self.pkh(script)
assert len(script_hash) == 20
return OP_HASH160.to_bytes(1) + len(script_hash).to_bytes(1) + script_hash + OP_EQUAL.to_bytes(1)
def encodeScriptDest(self, script_dest: bytes) -> str:
script_hash = script_dest[2:-1] # Extract hash from script
return self.sh_to_address(script_hash)
8 months ago
def getPubkeyHashDest(self, pkh: bytes) -> bytes:
# P2PKH
assert len(pkh) == 20
return OP_DUP.to_bytes(1) + OP_HASH160.to_bytes(1) + len(pkh).to_bytes(1) + pkh + OP_EQUALVERIFY.to_bytes(1) + OP_CHECKSIG.to_bytes(1)
def get_fee_rate(self, conf_target: int = 2) -> (float, str):
chain_client_settings = self._sc.getChainClientSettings(self.coin_type()) # basicswap.json
override_feerate = chain_client_settings.get('override_feerate', None)
if override_feerate:
self._log.debug('Fee rate override used for %s: %f', self.coin_name(), override_feerate)
return override_feerate, 'override_feerate'
min_relay_fee = chain_client_settings.get('min_relay_fee', None)
def try_get_fee_rate(self, conf_target):
# TODO: How to estimate required fee?
try:
fee_rate: float = self.rpc_wallet('walletinfo')['txfee']
assert (fee_rate > 0.0), 'Non positive feerate'
return fee_rate, 'paytxfee'
except Exception:
fee_rate: float = self.rpc('getnetworkinfo')['relayfee']
return fee_rate, 'relayfee'
fee_rate, rate_src = try_get_fee_rate(self, conf_target)
if min_relay_fee and min_relay_fee > fee_rate:
self._log.warning('Feerate {} ({}) is below min relay fee {} for {}'.format(self.format_amount(fee_rate, True, 1), rate_src, self.format_amount(min_relay_fee, True, 1), self.coin_name()))
return min_relay_fee, 'min_relay_fee'
return fee_rate, rate_src
def getNewAddress(self, use_segwit: bool = True, label: str = 'swap_receive') -> str:
return self.rpc_wallet('getnewaddress')
def getProofOfFunds(self, amount_for, extra_commit_bytes):
# TODO: Lock unspent and use same output/s to fund bid
unspents_by_addr = dict()
unspents = self.rpc_wallet('listunspent')
if unspents is None:
unspents = []
for u in unspents:
if u['spendable'] is not True:
continue
if u['address'] not in unspents_by_addr:
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
utxo_amount: int = self.make_int(u['amount'], r=1)
unspents_by_addr[u['address']]['total'] += utxo_amount
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout'], u['tree']))
max_utxos: int = 4
viable_addrs = []
for addr, data in unspents_by_addr.items():
if data['total'] >= amount_for:
# Sort from largest to smallest amount
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
# Max outputs required to reach amount_for
utxos_req: int = 0
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
utxos_req += 1
if sum_value >= amount_for:
break
if utxos_req <= max_utxos:
viable_addrs.append(addr)
continue
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
sign_for_addr: str = random.choice(viable_addrs)
self._log.debug('sign_for_addr %s', sign_for_addr)
prove_utxos = []
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
hasher = hashlib.sha256()
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
outpoint = (bytes.fromhex(utxo[1]), utxo[2], utxo[3])
prove_utxos.append(outpoint)
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
hasher.update(outpoint[2].to_bytes(1))
if sum_value >= amount_for:
break
utxos_hash = hasher.digest()
signature = self.rpc_wallet('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex()])
return (sign_for_addr, signature, prove_utxos)
def withdrawCoin(self, value: float, addr_to: str, subfee: bool = False) -> str:
if subfee:
raise ValueError('TODO')
params = [addr_to, value]
return self.rpc_wallet('sendtoaddress', params)
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc_wallet('validateaddress', [address])
return addr_info.get('ismine', False)
def encodeProofUtxos(self, proof_utxos):
packed_utxos = bytes()
for utxo in proof_utxos:
packed_utxos += utxo[0] + utxo[1].to_bytes(2, 'big') + utxo[2].to_bytes(1)
return packed_utxos
def decodeProofUtxos(self, msg_utxos):
proof_utxos = []
if len(msg_utxos) > 0:
num_utxos = len(msg_utxos) // 34
p: int = 0
for i in range(num_utxos):
proof_utxos.append((msg_utxos[p: p + 32], int.from_bytes(msg_utxos[p + 32: p + 34], 'big'), msg_utxos[p + 34]))
p += 35
return proof_utxos
def verifyProofOfFunds(self, address: str, signature: bytes, utxos, extra_commit_bytes: bytes):
hasher = hashlib.sha256()
sum_value: int = 0
for outpoint in utxos:
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
hasher.update(outpoint[2].to_bytes(1))
utxos_hash = hasher.digest()
passed = self.verifyMessage(address, address + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex(), signature)
ensure(passed is True, 'Proof of funds signature invalid')
sum_value: int = 0
for outpoint in utxos:
txout = self.rpc('gettxout', [outpoint[0].hex(), outpoint[1], outpoint[2]])
sum_value += self.make_int(txout['value'])
return sum_value
def verifyMessage(self, address: str, message: str, signature: str, message_magic: str = None) -> bool:
if message_magic is None:
message_magic = self.chainparams()['message_magic']
message_bytes = SerialiseNumCompact(len(message_magic)) + bytes(message_magic, 'utf-8') + SerialiseNumCompact(len(message)) + bytes(message, 'utf-8')
message_hash = blake256(message_bytes)
signature_bytes = base64.b64decode(signature)
rec_id = (signature_bytes[0] - 27) & 3
signature_bytes = signature_bytes[1:] + bytes((rec_id,))
try:
pubkey = PublicKey.from_signature_and_message(signature_bytes, message_hash, hasher=None)
except Exception as e:
self._log.info('verifyMessage failed: ' + str(e))
return False
address_hash = self.decode_address(address)[2:]
pubkey_hash = ripemd160(blake256(pubkey.format()))
return True if address_hash == pubkey_hash else False
def signTxWithWallet(self, tx) -> bytes:
return bytes.fromhex(self.rpc('signrawtransaction', [tx.hex()])['hex'])
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
# amount can't be a string, else: Failed to parse request: parameter #2 'amounts' must be type float64 (got string)
float_amount = float(self.format_amount(amount))
txn = self.rpc('createrawtransaction', [[], {addr_to: float_amount}])
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
options = {
'lockUnspents': lock_unspents,
'feeRate': fee_rate,
}
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc_wallet('fundrawtransaction', [txn, 'default', options])['hex']
def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc_wallet('signrawtransaction', [txn_funded])['hex']
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
if txid is None:
self._log.debug('TODO: getLockTxHeight')
return None
found_vout = None
# Search for txo at vout 0 and 1 if vout is not known
if vout is None:
test_range = range(2)
else:
test_range = (vout, )
for try_vout in test_range:
try:
txout = self.rpc('gettxout', [txid.hex(), try_vout, 0, True])
addresses = txout['scriptPubKey']['addresses']
if len(addresses) != 1 or addresses[0] != dest_address:
continue
if self.make_int(txout['value']) != bid_amount:
self._log.warning('getLockTxHeight found txout {} with incorrect amount {}'.format(txid.hex(), txout['value']))
continue
found_vout = try_vout
break
except Exception as e:
# self._log.warning('gettxout {}'.format(e))
return None
block_height: int = 0
confirmations: int = 0 if 'confirmations' not in txout else txout['confirmations']
# TODO: Better way?
if confirmations > 0:
block_height = self.getChainHeight() - confirmations
rv = {
'depth': confirmations,
'index': found_vout,
'height': block_height}
return rv
def find_prevout_info(self, txn_hex: str, txn_script: bytes):
txjs = self.rpc('decoderawtransaction', [txn_hex])
n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex())
txo = txjs['vout'][n]
return {
'txid': txjs['txid'],
'vout': n,
'scriptPubKey': txo['scriptPubKey']['hex'],
'redeemScript': txn_script.hex(),
'amount': txo['value'],
}
def getHTLCSpendTxVSize(self, redeem: bool = True) -> int:
tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes
tx_vsize += 348 if redeem else 316
return tx_vsize
def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes = None) -> str:
tx = CTransaction()
tx.version = self.txVersion()
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'], 0)))
pkh = self.decode_address(output_addr)[2:]
script = self.getPubkeyHashDest(pkh)
tx.vout.append(self.txoType()(output_value, script))
return tx.serialize().hex()
def createRefundTxn(self, prevout, output_addr: str, output_value: int, locktime: int, sequence: int, txn_script: bytes = None) -> str:
tx = CTransaction()
tx.version = self.txVersion()
tx.locktime = locktime
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'], 0), sequence=sequence,))
pkh = self.decode_address(output_addr)[2:]
script = self.getPubkeyHashDest(pkh)
tx.vout.append(self.txoType()(output_value, script))
return tx.serialize().hex()
def verifyRawTransaction(self, tx_hex: str, prevouts):
inputs_valid: bool = True
validscripts: int = 0
tx_bytes = bytes.fromhex(tx_hex)
tx = self.loadTx(bytes.fromhex(tx_hex))
for i, txi in enumerate(tx.vin):
prevout_data = prevouts[i]
redeem_script = bytes.fromhex(prevout_data['redeemScript'])
prevout_value = self.make_int(prevout_data['amount'])
sig, pk = extract_sig_and_pk(txi.signature_script)
if not sig or not pk:
self._log.warning(f'verifyRawTransaction failed to extract signature for input {i}')
continue
if self.verifyTxSig(tx_bytes, sig, pk, i, redeem_script, prevout_value):
validscripts += 1
# TODO: validate inputs
inputs_valid = True
return {
'inputs_valid': inputs_valid,
'validscripts': validscripts,
}
def getBlockHeaderFromHeight(self, height):
block_hash = self.rpc('getblockhash', [height])
return self.rpc('getblockheader', [block_hash])
def getBlockWithTxns(self, block_hash: str):
block = self.rpc('getblock', [block_hash, True, True])
return {
'hash': block['hash'],
'previousblockhash': block['previousblockhash'],
'tx': block['rawtx'],
'confirmations': block['confirmations'],
'height': block['height'],
'time': block['time'],
'version': block['version'],
'merkleroot': block['merkleroot'],
}
def publishTx(self, tx: bytes):
return self.rpc('sendrawtransaction', [tx.hex()])
def describeTx(self, tx_hex: str):
return self.rpc('decoderawtransaction', [tx_hex])