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:
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")
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 = {}
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"
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")
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()