rostrum 14.0.1

An efficient implementation of Electrum Server with token support
Documentation
#!/usr/bin/env python3
# Copyright (c) 2022 The Bitcoin developers
# Copyright (c) 2022 The Bitcoin Unlimited developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from decimal import Decimal
import logging

from .nodemessages import COIN, CTransaction
from ..serialize import from_hex
from ..environment import node as node_software, Node

DUST_AMOUNT = 1000
DUST_AMOUNT_COIN = Decimal(DUST_AMOUNT) / Decimal(COIN)


def get_key(utxo):
    return f'{utxo["txid"]}:{utxo["vout"]}'


def get_total_amount(utxos) -> Decimal:
    if isinstance(utxos, dict):
        it = utxos.values()
    else:
        it = utxos
    return sum(Decimal(utxo["amount"]) for utxo in it)


def get_token_amount(utxos) -> int:
    """
    Assumes all utxos are for one specific token
    """

    def has_token_data(x):
        return "tokenData" in x

    if isinstance(utxos, dict):
        it = filter(has_token_data, utxos.values())
    else:
        it = filter(has_token_data, utxos)

    return sum(int(utxo["tokenData"]["amount"]) for utxo in it)


def createrawtransaction(node, inputs, outputs, change_outputs):
    if node_software() == Node.BCHN:
        return node.createrawtransaction(inputs, outputs + change_outputs)

    raise Exception("NYI")


def signrawtransaction(node, txhex, flags):
    if node_software() == Node.BCHN:
        return node.signrawtransaction(txhex, None, flags)

    raise Exception("NYI")


# Example use:
#   tx = self.create_token_genesis_tx(self.nodes[0], addrs[1], 1, 123456, nft=bytes.fromhex("beeff00d"))
# pylint: disable=too-many-locals,too-many-arguments,too-many-positional-arguments
def create_token_genesis_tx(
    node,
    to_addr,
    bch_amount: Decimal,
    token_amount,
    nft=None,
    return_txid=False,
):
    fee = Decimal("0.00001")
    capability = "minting"

    utxos = {}
    # Find a genesis utxo we can use
    for utxo in node.listunspent():
        if utxo["vout"] == 0:
            utxos[get_key(utxo)] = utxo
            token_id = utxo["txid"]
            break
    else:
        assert False, "No genesis-capable UTXOs found in node wallet"
    # Add more utxos to meet amount predicate
    while get_total_amount(utxos) < bch_amount + fee:
        for utxo in node.listunspent():
            key = get_key(utxo)
            if key not in utxos:
                utxos[key] = utxo
                break
        else:
            assert False, "Not enough funds"
    utxo_0 = list(utxos.values())[0]
    change_addr = utxo_0["address"]
    token_data = {"category": token_id, "amount": str(token_amount)}
    if nft is not None:
        token_data["nft"] = {"capability": capability, "commitment": nft.hex()}
    amount_in = get_total_amount(utxos)
    amount_change = amount_in - bch_amount - fee
    assert amount_change >= 0
    change_outs = []
    if amount_change > DUST_AMOUNT_COIN:
        change_outs.append({change_addr: amount_change})

    txhex = createrawtransaction(
        node,
        [{"txid": u["txid"], "vout": u["vout"]} for u in utxos.values()],
        [{to_addr: {"amount": f"{bch_amount:.10f}", "tokenData": token_data}}],
        change_outs,
    )

    tx = from_hex(CTransaction(), txhex)
    for i, txout in enumerate(tx.vout):
        dust = DUST_AMOUNT
        assert (
            txout.n_value >= dust
        ), f"Dust limit: output {i} is below {dust} ({txout})"
        res = signrawtransaction(node, txhex, "ALL|FORKID|UTXOS")
    assert res["complete"]
    txhex = res["hex"]
    fee_sats = int(fee * COIN)
    assert (
        len(txhex) // 2 <= fee_sats
    ), f"Paid too little fee ({fee_sats}) for txn of size {len(txhex) // 2}"
    txid = node.sendrawtransaction(txhex)
    logging.info("Minted token in tx: %s", txid)
    if return_txid:
        return token_id, txid
    return token_id


def token_utxos(node, token_id):
    def token_filter(x):
        return x["tokenData"]["category"] == token_id

    def list_unspent():
        assert node_software() == Node.BCHN
        return node.listunspent(0, None, None, True, {"tokensOnly": True})

    return list(filter(token_filter, list_unspent()))


def bch_utxos(node, min_input):
    all_utxos = node.listunspent(0)
    amount = Decimal(0)

    collected = []
    for u in all_utxos:
        amount += u["amount"]
        collected.append(u)
        if amount >= min_input:
            return collected

    assert amount >= min_input, f"{amount} < {min_input} utxos: {all_utxos}"
    return collected


def send_token(node, token_id, to_address, amount):
    fee = Decimal("0.00001")
    utxos = token_utxos(node, token_id) + bch_utxos(node, DUST_AMOUNT_COIN * 3)

    bch_amount_in = get_total_amount(utxos)
    token_amount_in = get_token_amount(utxos)
    assert token_amount_in >= amount

    if token_amount_in > amount:
        token_change = {"category": token_id, "amount": str(token_amount_in - amount)}

        change_outs = [
            {
                node.getnewaddress(): {
                    "amount": bch_amount_in - DUST_AMOUNT_COIN - fee,
                    "tokenData": token_change,
                }
            }
        ]
    else:
        change_outs = [{node.getnewaddress(): bch_amount_in - DUST_AMOUNT_COIN - fee}]

    our_token_data = {"category": token_id, "amount": str(amount)}
    txhex = createrawtransaction(
        node,
        [{"txid": u["txid"], "vout": u["vout"]} for u in utxos],
        [{to_address: {"amount": DUST_AMOUNT_COIN, "tokenData": our_token_data}}],
        change_outs,
    )

    res = signrawtransaction(node, txhex, "ALL|FORKID|UTXOS")

    txhex = res["hex"]
    txid = node.sendrawtransaction(txhex)
    return txid


def create_nft(node, parent_id, commitment: bytearray, to_address):
    fee = Decimal("0.00001")
    # Just take all utxos of the parent token as input, one of them should have
    # minting capability.
    utxos = token_utxos(node, parent_id) + bch_utxos(node, DUST_AMOUNT_COIN * 3)

    bch_amount_in = get_total_amount(utxos)
    token_amount_in = get_token_amount(utxos)

    token_change = {
        "category": parent_id,
        "amount": str(token_amount_in),
        "nft": {
            "capability": "minting",
            "commitment": bytearray("minting", "utf8").hex(),
        },
    }

    change_outs = [
        {
            node.getnewaddress(): {
                "amount": bch_amount_in - DUST_AMOUNT_COIN - fee,
                "tokenData": token_change,
            }
        }
    ]

    our_token_data = {
        "category": parent_id,
        "amount": str(0),
        "nft": {
            "capability": "minting",
            "commitment": commitment.hex(),
        },
    }

    txhex = createrawtransaction(
        node,
        [{"txid": u["txid"], "vout": u["vout"]} for u in utxos],
        [{to_address: {"amount": DUST_AMOUNT_COIN, "tokenData": our_token_data}}],
        change_outs,
    )

    res = signrawtransaction(node, txhex, "ALL|FORKID|UTXOS")

    txhex = res["hex"]
    node.sendrawtransaction(txhex)
    return commitment.hex()