Add OP_RETURN support for heirs
Allow heirs to produce an OP_RETURN output instead of a regular BTC payment. An address prefixed with OP_RETURN: carries hex data (max 80 bytes); such heirs always have amount 0 and are excluded from amount calculations. GUI dialog shows a message field with the decoded text, the heir list displays the decoded message, and the OP_RETURN output is emitted with zero value in inheritance transactions.
This commit is contained in:
@@ -75,6 +75,8 @@ HEIR_REAL_AMOUNT = 3 # resolved amount once percentages are computed
|
||||
HEIR_DUST_AMOUNT = 4 # amount when below dust threshold (marked "DUST: ...")
|
||||
TRANSACTION_LABEL = "inheritance transaction"
|
||||
|
||||
OP_RETURN_PREFIX = "OP_RETURN:"
|
||||
|
||||
|
||||
class AliasNotFoundException(Exception):
|
||||
pass
|
||||
@@ -86,6 +88,27 @@ def reduce_outputs(in_amount, out_amount, fee, outputs):
|
||||
output.value = math.floor((in_amount - fee) / out_amount * output.value)
|
||||
|
||||
|
||||
def is_op_return_address(address: str) -> bool:
|
||||
return str(address).startswith(OP_RETURN_PREFIX)
|
||||
|
||||
|
||||
def get_op_return_hex(address: str) -> Optional[str]:
|
||||
if is_op_return_address(address):
|
||||
return address[len(OP_RETURN_PREFIX):]
|
||||
return None
|
||||
|
||||
|
||||
def validate_op_return_hex(data_hex: str) -> None:
|
||||
try:
|
||||
data = bytes.fromhex(data_hex)
|
||||
except ValueError:
|
||||
raise NotAnAddress(f"OP_RETURN data is not valid hex: {data_hex}")
|
||||
if len(data) > 80:
|
||||
raise NotAnAddress(
|
||||
f"OP_RETURN data too long ({len(data)} bytes, max 80)"
|
||||
)
|
||||
|
||||
|
||||
def create_op_return_script(data_hex: str) -> bytes:
|
||||
"""Crea scriptpubkey OP_RETURN in bytes"""
|
||||
data = bytes.fromhex(data_hex)
|
||||
@@ -132,14 +155,22 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
|
||||
heir[HEIR_REAL_AMOUNT]
|
||||
):
|
||||
try:
|
||||
real_amount = heir[HEIR_REAL_AMOUNT]
|
||||
outputs.append(
|
||||
PartialTxOutput.from_address_and_value(
|
||||
heir[HEIR_ADDRESS], real_amount
|
||||
if is_op_return_address(heir[HEIR_ADDRESS]):
|
||||
data_hex = heir[HEIR_ADDRESS][len(OP_RETURN_PREFIX):]
|
||||
op_return_script = create_op_return_script(data_hex)
|
||||
outputs.append(
|
||||
PartialTxOutput(value=0, scriptpubkey=op_return_script)
|
||||
)
|
||||
)
|
||||
out_amount += real_amount
|
||||
description += f"{name}\n"
|
||||
description += f"{name}\n"
|
||||
else:
|
||||
real_amount = heir[HEIR_REAL_AMOUNT]
|
||||
outputs.append(
|
||||
PartialTxOutput.from_address_and_value(
|
||||
heir[HEIR_ADDRESS], real_amount
|
||||
)
|
||||
)
|
||||
out_amount += real_amount
|
||||
description += f"{name}\n"
|
||||
except BitcoinException as e:
|
||||
_logger.info("exception decoding output {} - {}".format(type(e), e))
|
||||
heir[HEIR_REAL_AMOUNT] = e
|
||||
@@ -276,11 +307,12 @@ def print_transaction(heirs, tx, locktimes, tx_fees):
|
||||
heirname = ""
|
||||
for key in heirs.keys():
|
||||
heir = heirs[key]
|
||||
if heir[HEIR_ADDRESS] == out["address"] and str(heir[HEIR_LOCKTIME]) == str(
|
||||
out_address = out.get("address", "OP_RETURN")
|
||||
if heir[HEIR_ADDRESS] == out_address and str(heir[HEIR_LOCKTIME]) == str(
|
||||
jtx["locktime"]
|
||||
):
|
||||
heirname = key
|
||||
print(f"{heirname}\t{out['address']}: {out['value_sats']}")
|
||||
print(f"{heirname}\t{out.get('address', 'OP_RETURN')}: {out['value_sats']}")
|
||||
|
||||
print()
|
||||
size = tx.estimated_size()
|
||||
@@ -401,6 +433,9 @@ class Heirs(dict, Logger):
|
||||
amount = 0
|
||||
for key, v in heir_list.items():
|
||||
try:
|
||||
if is_op_return_address(v[HEIR_ADDRESS]):
|
||||
heir_list[key].insert(HEIR_REAL_AMOUNT, 0)
|
||||
continue
|
||||
column = HEIR_AMOUNT
|
||||
if real:
|
||||
column = HEIR_REAL_AMOUNT
|
||||
@@ -452,6 +487,12 @@ class Heirs(dict, Logger):
|
||||
)
|
||||
)
|
||||
continue
|
||||
if is_op_return_address(self[key][HEIR_ADDRESS]):
|
||||
heir = list(self[key])
|
||||
heir.insert(HEIR_REAL_AMOUNT, 0)
|
||||
fixed_heirs[key] = heir
|
||||
_logger.debug(f"OP_RETURN heir {key} excluded from amount calculation")
|
||||
continue
|
||||
if Util.is_perc(self[key][HEIR_AMOUNT]):
|
||||
percent_amount += float(self[key][HEIR_AMOUNT][:-1])
|
||||
percent_heirs[key] = list(self[key])
|
||||
@@ -592,6 +633,8 @@ class Heirs(dict, Logger):
|
||||
heir[HEIR_REAL_AMOUNT]
|
||||
):
|
||||
valid_real_heirs += 1
|
||||
elif len(heir) > HEIR_REAL_AMOUNT and is_op_return_address(heir[HEIR_ADDRESS]):
|
||||
valid_real_heirs += 1
|
||||
if real_heirs > 0 and valid_real_heirs == 0:
|
||||
raise HeirAmountIsDustException(
|
||||
"All heirs' shares are below the dust limit"
|
||||
@@ -811,6 +854,10 @@ class Heirs(dict, Logger):
|
||||
return None
|
||||
|
||||
def validate_address(address):
|
||||
if is_op_return_address(address):
|
||||
data_hex = address[len(OP_RETURN_PREFIX):]
|
||||
validate_op_return_hex(data_hex)
|
||||
return address
|
||||
if not bitcoin.is_address(address, net=constants.net):
|
||||
raise NotAnAddress(f"not an address,{address}")
|
||||
return address
|
||||
@@ -835,7 +882,10 @@ class Heirs(dict, Logger):
|
||||
|
||||
def validate_heir(k, v, timestamp_to_check=False):
|
||||
address = Heirs.validate_address(v[HEIR_ADDRESS])
|
||||
amount = Heirs.validate_amount(v[HEIR_AMOUNT])
|
||||
if is_op_return_address(v[HEIR_ADDRESS]):
|
||||
amount = "0"
|
||||
else:
|
||||
amount = Heirs.validate_amount(v[HEIR_AMOUNT])
|
||||
locktime = Heirs.validate_locktime(v[HEIR_LOCKTIME], timestamp_to_check)
|
||||
return (address, amount, locktime)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user