346 lines
12 KiB
Python
346 lines
12 KiB
Python
import io
|
|
import six
|
|
import logging
|
|
from binascii import hexlify
|
|
from typing import List
|
|
|
|
from lbrynet.wallet import get_wallet_manager
|
|
from lbrynet.wallet.bcd_data_stream import BCDataStream
|
|
from lbrynet.wallet.hash import sha256, hash160_to_address, claim_id_hash
|
|
from lbrynet.wallet.script import InputScript, OutputScript
|
|
from lbrynet.wallet.wallet import Wallet
|
|
|
|
|
|
log = logging.getLogger()
|
|
|
|
|
|
NULL_HASH = '\x00'*32
|
|
|
|
|
|
class InputOutput(object):
|
|
|
|
@property
|
|
def fee(self):
|
|
""" Fee based on size of the input / output. """
|
|
return get_wallet_manager().fee_per_byte * self.size
|
|
|
|
@property
|
|
def size(self):
|
|
""" Size of this input / output in bytes. """
|
|
stream = BCDataStream()
|
|
self.serialize_to(stream)
|
|
return len(stream.get_bytes())
|
|
|
|
def serialize_to(self, stream):
|
|
raise NotImplemented
|
|
|
|
|
|
class Input(InputOutput):
|
|
|
|
NULL_SIGNATURE = '0'*72
|
|
NULL_PUBLIC_KEY = '0'*33
|
|
|
|
def __init__(self, output_or_txid_index, script, sequence=0xFFFFFFFF):
|
|
if isinstance(output_or_txid_index, Output):
|
|
self.output = output_or_txid_index # type: Output
|
|
self.output_txid = self.output.transaction.hash
|
|
self.output_index = self.output.index
|
|
else:
|
|
self.output = None # type: Output
|
|
self.output_txid, self.output_index = output_or_txid_index
|
|
self.sequence = sequence
|
|
self.is_coinbase = self.output_txid == NULL_HASH
|
|
self.coinbase = script if self.is_coinbase else None
|
|
self.script = script if not self.is_coinbase else None # type: InputScript
|
|
|
|
def link_output(self, output):
|
|
assert self.output is None
|
|
assert self.output_txid == output.transaction.id
|
|
assert self.output_index == output.index
|
|
self.output = output
|
|
|
|
@property
|
|
def amount(self):
|
|
""" Amount this input adds to the transaction. """
|
|
if self.output is None:
|
|
raise ValueError('Cannot get input value without referenced output.')
|
|
return self.output.amount
|
|
|
|
@property
|
|
def effective_amount(self):
|
|
""" Amount minus fee. """
|
|
return self.amount - self.fee
|
|
|
|
def __lt__(self, other):
|
|
return self.effective_amount < other.effective_amount
|
|
|
|
@classmethod
|
|
def deserialize_from(cls, stream):
|
|
txid = stream.read(32)
|
|
index = stream.read_uint32()
|
|
script = stream.read_string()
|
|
sequence = stream.read_uint32()
|
|
return cls(
|
|
(txid, index),
|
|
InputScript(script) if not txid == NULL_HASH else script,
|
|
sequence
|
|
)
|
|
|
|
def serialize_to(self, stream, alternate_script=None):
|
|
stream.write(self.output_txid)
|
|
stream.write_uint32(self.output_index)
|
|
if alternate_script is not None:
|
|
stream.write_string(alternate_script)
|
|
else:
|
|
if self.is_coinbase:
|
|
stream.write_string(self.coinbase)
|
|
else:
|
|
stream.write_string(self.script.source)
|
|
stream.write_uint32(self.sequence)
|
|
|
|
def to_python_source(self):
|
|
return (
|
|
u"InputScript(\n"
|
|
u" (output_txid=unhexlify('{}'), output_index={}),\n"
|
|
u" script=unhexlify('{}')\n"
|
|
u" # tokens: {}\n"
|
|
u")").format(
|
|
hexlify(self.output_txid), self.output_index,
|
|
hexlify(self.coinbase) if self.is_coinbase else hexlify(self.script.source),
|
|
repr(self.script.tokens)
|
|
)
|
|
|
|
|
|
class Output(InputOutput):
|
|
|
|
def __init__(self, transaction, index, amount, script):
|
|
self.transaction = transaction # type: Transaction
|
|
self.index = index # type: int
|
|
self.amount = amount # type: int
|
|
self.script = script # type: OutputScript
|
|
self._effective_amount = None # type: int
|
|
|
|
def __lt__(self, other):
|
|
return self.effective_amount < other.effective_amount
|
|
|
|
def _add_and_return(self):
|
|
self.transaction.add_outputs([self])
|
|
return self
|
|
|
|
@classmethod
|
|
def pay_pubkey_hash(cls, transaction, index, amount, pubkey_hash):
|
|
return cls(
|
|
transaction, index, amount,
|
|
OutputScript.pay_pubkey_hash(pubkey_hash)
|
|
)._add_and_return()
|
|
|
|
@classmethod
|
|
def pay_claim_name_pubkey_hash(cls, transaction, index, amount, claim_name, claim, pubkey_hash):
|
|
return cls(
|
|
transaction, index, amount,
|
|
OutputScript.pay_claim_name_pubkey_hash(claim_name, claim, pubkey_hash)
|
|
)._add_and_return()
|
|
|
|
def spend(self, signature=Input.NULL_SIGNATURE, pubkey=Input.NULL_PUBLIC_KEY):
|
|
""" Create the input to spend this output."""
|
|
assert self.script.is_pay_pubkey_hash, 'Attempting to spend unsupported output.'
|
|
script = InputScript.redeem_pubkey_hash(signature, pubkey)
|
|
return Input(self, script)
|
|
|
|
@property
|
|
def effective_amount(self):
|
|
""" Amount minus fees it would take to spend this output. """
|
|
if self._effective_amount is None:
|
|
txi = self.spend()
|
|
self._effective_amount = txi.effective_amount
|
|
return self._effective_amount
|
|
|
|
@classmethod
|
|
def deserialize_from(cls, stream, transaction, index):
|
|
return cls(
|
|
transaction=transaction,
|
|
index=index,
|
|
amount=stream.read_uint64(),
|
|
script=OutputScript(stream.read_string())
|
|
)
|
|
|
|
def serialize_to(self, stream):
|
|
stream.write_uint64(self.amount)
|
|
stream.write_string(self.script.source)
|
|
|
|
def to_python_source(self):
|
|
return (
|
|
u"OutputScript(tx, index={}, amount={},\n"
|
|
u" script=unhexlify('{}')\n"
|
|
u" # tokens: {}\n"
|
|
u")").format(
|
|
self.index, self.amount, hexlify(self.script.source), repr(self.script.tokens))
|
|
|
|
|
|
class Transaction:
|
|
|
|
def __init__(self, raw=None, version=1, locktime=0, height=None, is_saved=False):
|
|
self._raw = raw
|
|
self._hash = None
|
|
self._id = None
|
|
self.version = version # type: int
|
|
self.locktime = locktime # type: int
|
|
self.height = height # type: int
|
|
self.inputs = [] # type: List[Input]
|
|
self.outputs = [] # type: List[Output]
|
|
self.is_saved = is_saved # type: bool
|
|
if raw is not None:
|
|
self._deserialize()
|
|
|
|
@property
|
|
def id(self):
|
|
if self._id is None:
|
|
self._id = self.hash[::-1]
|
|
return self._id
|
|
|
|
@property
|
|
def hash(self):
|
|
if self._hash is None:
|
|
self._hash = sha256(sha256(self.raw))
|
|
return self._hash
|
|
|
|
@property
|
|
def raw(self):
|
|
if self._raw is None:
|
|
self._raw = self._serialize()
|
|
return self._raw
|
|
|
|
def _reset(self):
|
|
self._raw = None
|
|
self._hash = None
|
|
self._id = None
|
|
|
|
def get_claim_id(self, output_index):
|
|
script = self.outputs[output_index]
|
|
assert script.script.is_claim_name(), 'Not a name claim.'
|
|
return claim_id_hash(self.hash, output_index)
|
|
|
|
@property
|
|
def is_complete(self):
|
|
s, r = self.signature_count()
|
|
return r == s
|
|
|
|
@property
|
|
def fee(self):
|
|
""" Fee that will actually be paid."""
|
|
return self.input_sum - self.output_sum
|
|
|
|
@property
|
|
def size(self):
|
|
""" Size in bytes of the entire transaction. """
|
|
return len(self.raw)
|
|
|
|
@property
|
|
def base_size(self):
|
|
""" Size in bytes of transaction meta data and all outputs; without inputs. """
|
|
return len(self._serialize(with_inputs=False))
|
|
|
|
@property
|
|
def base_fee(self):
|
|
""" Fee for the transaction header and all outputs; without inputs. """
|
|
byte_fee = get_wallet_manager().fee_per_byte * self.base_size
|
|
return max(byte_fee, self.claim_name_fee)
|
|
|
|
@property
|
|
def claim_name_fee(self):
|
|
char_fee = get_wallet_manager().fee_per_name_char
|
|
fee = 0
|
|
for output in self.outputs:
|
|
if output.script.is_claim_name:
|
|
fee += len(output.script.values['claim_name']) * char_fee
|
|
return fee
|
|
|
|
def _serialize(self, with_inputs=True):
|
|
stream = BCDataStream()
|
|
stream.write_uint32(self.version)
|
|
if with_inputs:
|
|
stream.write_compact_size(len(self.inputs))
|
|
for txin in self.inputs:
|
|
txin.serialize_to(stream)
|
|
stream.write_compact_size(len(self.outputs))
|
|
for txout in self.outputs:
|
|
txout.serialize_to(stream)
|
|
stream.write_uint32(self.locktime)
|
|
return stream.get_bytes()
|
|
|
|
def _serialize_for_signature(self, signing_input):
|
|
stream = BCDataStream()
|
|
stream.write_uint32(self.version)
|
|
stream.write_compact_size(len(self.inputs))
|
|
for i, txin in enumerate(self.inputs):
|
|
if signing_input == i:
|
|
txin.serialize_to(stream, txin.output.script.source)
|
|
else:
|
|
txin.serialize_to(stream, b'')
|
|
stream.write_compact_size(len(self.outputs))
|
|
for txout in self.outputs:
|
|
txout.serialize_to(stream)
|
|
stream.write_uint32(self.locktime)
|
|
stream.write_uint32(1) # signature hash type: SIGHASH_ALL
|
|
return stream.get_bytes()
|
|
|
|
def _deserialize(self):
|
|
if self._raw is not None:
|
|
stream = BCDataStream(self._raw)
|
|
self.version = stream.read_uint32()
|
|
input_count = stream.read_compact_size()
|
|
self.inputs = [Input.deserialize_from(stream) for _ in range(input_count)]
|
|
output_count = stream.read_compact_size()
|
|
self.outputs = [Output.deserialize_from(stream, self, i) for i in range(output_count)]
|
|
self.locktime = stream.read_uint32()
|
|
|
|
def add_inputs(self, inputs):
|
|
self.inputs.extend(inputs)
|
|
self._reset()
|
|
|
|
def add_outputs(self, outputs):
|
|
self.outputs.extend(outputs)
|
|
self._reset()
|
|
|
|
def sign(self, wallet): # type: (Wallet) -> bool
|
|
for i, txi in enumerate(self.inputs):
|
|
txo_script = txi.output.script
|
|
if txo_script.is_pay_pubkey_hash:
|
|
address = hash160_to_address(txo_script.values['pubkey_hash'], wallet.chain)
|
|
private_key = wallet.get_private_key_for_address(address)
|
|
tx = self._serialize_for_signature(i)
|
|
txi.script.values['signature'] = private_key.sign(tx)+six.int2byte(1)
|
|
txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes
|
|
txi.script.generate()
|
|
self._reset()
|
|
return True
|
|
|
|
def sort(self):
|
|
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki
|
|
self.inputs.sort(key=lambda i: (i['prevout_hash'], i['prevout_n']))
|
|
self.outputs.sort(key=lambda o: (o[2], pay_script(o[0], o[1])))
|
|
|
|
@property
|
|
def input_sum(self):
|
|
return sum(i.amount for i in self.inputs)
|
|
|
|
@property
|
|
def output_sum(self):
|
|
return sum(o.amount for o in self.outputs)
|
|
|
|
def to_python_source(self):
|
|
s = io.StringIO()
|
|
s.write(u'tx = Transaction(version={}, locktime={}, height={})\n'.format(
|
|
self.version, self.locktime, self.height
|
|
))
|
|
for txi in self.inputs:
|
|
s.write(u'tx.add_input(')
|
|
s.write(txi.to_python_source())
|
|
s.write(u')\n')
|
|
for txo in self.outputs:
|
|
s.write(u'tx.add_output(')
|
|
s.write(txo.to_python_source())
|
|
s.write(u')\n')
|
|
s.write(u'# tx.id: unhexlify("{}")\n'.format(hexlify(self.id)))
|
|
s.write(u'# tx.raw: unhexlify("{}")\n'.format(hexlify(self.raw)))
|
|
return s.getvalue()
|