#!/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) if node_software() == Node.BCHUNLIMITED: outputs = outputs[0] for change in change_outputs: outputs.update(change) return node.createrawtransaction(inputs, outputs) raise Exception("NYI") def signrawtransaction(node, txhex, flags): if node_software() == Node.BCHN: return node.signrawtransaction(txhex, None, flags) if node_software() == Node.BCHUNLIMITED: return node.signrawtransaction(txhex, None, 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 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(): if node_software() == Node.BCHUNLIMITED: return node.listunspent(0, 9999999, [], {"tokensOnly": True}) 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()