diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 1b2393933e7..959f82e2971 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -107,6 +107,7 @@ 'feature_fedpeg.py', 'default_asset_name.py', 'assetdir.py', + 'feature_issuance.py', # Elements' specially adapted tests second 'blockchain.py', diff --git a/qa/rpc-tests/confidential_transactions.py b/qa/rpc-tests/confidential_transactions.py index f45e36d402f..c81d94db22a 100755 --- a/qa/rpc-tests/confidential_transactions.py +++ b/qa/rpc-tests/confidential_transactions.py @@ -22,9 +22,7 @@ def setup_network(self, split=False): self.sync_all() def run_test(self): - print("Mining blocks...") - self.nodes[0].generate(101) - self.sync_all() + print("General Confidential tests") #Running balances node0 = self.nodes[0].getbalance()["bitcoin"] node1 = 0 @@ -217,9 +215,6 @@ def run_test(self): # Unblinded issuance of asset issued = self.nodes[0].issueasset(1, 1, False) - assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["asset"]], 1) - assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["token"]], 1) - # Quick unblinded reissuance check, making 2*COIN total self.nodes[0].reissueasset(issued["asset"], 1) # Compare resulting fields with getrawtransaction @@ -228,7 +223,6 @@ def run_test(self): assert_equal(issued["asset"], raw_details["vin"][issued["vin"]]["issuance"]["asset"]) assert_equal(issued["token"], raw_details["vin"][issued["vin"]]["issuance"]["token"]) - testAssetHex = issued["asset"] self.nodes[0].generate(1) self.sync_all() @@ -275,46 +269,6 @@ def run_test(self): self.nodes[2].generate(101) self.sync_all() - # Destroy assets - pre_destroy_btc_balance = self.nodes[2].getwalletinfo()['balance']['bitcoin'] - self.nodes[2].destroyamount('bitcoin', 2) # Destroy 2 BTC - self.nodes[2].generate(1) - self.sync_all() - - issuedamount = self.nodes[0].getwalletinfo()['balance'][issued["token"]] - assert_equal(issuedamount, Decimal('1.0')) - self.nodes[0].destroyamount(issued["token"], issuedamount) # Destroy all reissuance tokens of one type - - self.nodes[0].generate(1) - self.sync_all() - assert(issued["token"] not in self.nodes[0].getinfo()['balance']) - - # Test various issuance and auditing paths - - issuancedata = self.nodes[0].issueasset(Decimal('0.00000002'), Decimal('0.00000001')) #2 of asset, 1 reissuance token - self.nodes[1].generate(1) - self.sync_all() - assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["asset"]], Decimal('0.00000002')) - assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000001')) - self.nodes[0].reissueasset(issuancedata["asset"], Decimal('0.00000001')) - self.sync_all() - assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["asset"]], Decimal('0.00000003')) - # Can't reissue an issuance token (yet) - try: - self.nodes[0].reissueasset(issuancedata["token"], Decimal('0.00000001')) - raise AssertionError("You shouldn't be able to reissue a token yet") - except JSONRPCException: - pass - - - issuancedata = self.nodes[2].issueasset(Decimal('0.00000005'), 0) #5 of asset, 0 reissuance token - # No reissuance tokens - try: - self.nodes[2].reissueasset(issuancedata["token"], 5) - raise AssertionError("You shouldn't be able to reissue without a token") - except JSONRPCException: - pass - issuancedata = self.nodes[2].issueasset(0, Decimal('0.00000006')) #0 of asset, 6 reissuance token # Node 2 will send node 1 a reissuance token, both will generate assets @@ -325,68 +279,13 @@ def run_test(self): self.nodes[2].generate(1) self.sync_all() - assert_equal(self.nodes[2].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000005')) - assert_equal(self.nodes[1].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000001')) redata1 = self.nodes[1].reissueasset(issuancedata["asset"], Decimal('0.05')) redata2 = self.nodes[2].reissueasset(issuancedata["asset"], Decimal('0.025')) - - self.sync_all() - # Watch-only issuances won't show up in wallet until confirmed self.nodes[1].generate(1) self.sync_all() - # Now have node 0 audit these issuances - blindingkey1 = self.nodes[1].dumpissuanceblindingkey(redata1["txid"], redata1["vin"]) - blindingkey2 = self.nodes[2].dumpissuanceblindingkey(redata2["txid"], redata2["vin"]) - blindingkey3 = self.nodes[2].dumpissuanceblindingkey(issuancedata["txid"], issuancedata["vin"]) - - # Need addr to get transactions in wallet. TODO: importissuances? - txdet1 = self.nodes[1].gettransaction(redata1["txid"])["details"] - txdet2 = self.nodes[2].gettransaction(redata2["txid"])["details"] - txdet3 = self.nodes[2].gettransaction(issuancedata["txid"])["details"] - - # Receive addresses added last - addr1 = txdet1[len(txdet1)-1]["address"] - addr2 = txdet2[len(txdet2)-1]["address"] - addr3 = txdet3[len(txdet3)-1]["address"] - - assert_equal(len(self.nodes[0].listissuances()), 6); - self.nodes[0].importaddress(addr1) - self.nodes[0].importaddress(addr2) - self.nodes[0].importaddress(addr3) - - issuances = self.nodes[0].listissuances() - assert_equal(len(issuances), 9) - - for issue in issuances: - if issue['txid'] == redata1["txid"] and issue['vin'] == redata1["vin"]: - assert_equal(issue['assetamount'], Decimal('-1')) - if issue['txid'] == redata2["txid"] and issue['vin'] == redata2["vin"]: - assert_equal(issue['assetamount'], Decimal('-1')) - if issue['txid'] == issuancedata["txid"] and issue['vin'] == issuancedata["vin"]: - assert_equal(issue['assetamount'], Decimal('-1')) - assert_equal(issue['tokenamount'], Decimal('-1')) - - self.nodes[0].importissuanceblindingkey(redata1["txid"], redata1["vin"], blindingkey1) - self.nodes[0].importissuanceblindingkey(redata2["txid"], redata2["vin"], blindingkey2) - self.nodes[0].importissuanceblindingkey(issuancedata["txid"], issuancedata["vin"], blindingkey3) - - issuances = self.nodes[0].listissuances() - - for issue in issuances: - if issue['txid'] == redata1["txid"] and issue['vin'] == redata1["vin"]: - assert_equal(issue['assetamount'], Decimal('0.05')) - if issue['txid'] == redata2["txid"] and issue['vin'] == redata2["vin"]: - assert_equal(issue['assetamount'], Decimal('0.025')) - if issue['txid'] == issuancedata["txid"] and issue['vin'] == issuancedata["vin"]: - assert_equal(issue['assetamount'], Decimal('0')) - assert_equal(issue['tokenamount'], Decimal('0.00000006')) - # Check for value accounting when asset issuance is null but token not, ie unblinded issued = self.nodes[0].issueasset(0, 1, False) - assert(issued["asset"] not in self.nodes[0].getwalletinfo()["balance"]) - assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["token"]], 1) - # Check for value when receiving defferent assets by same address. self.nodes[0].sendtoaddress(unconfidential_address2, Decimal('0.00000001'), "", "", False, test_asset) diff --git a/qa/rpc-tests/feature_issuance.py b/qa/rpc-tests/feature_issuance.py new file mode 100755 index 00000000000..ea3284cfac3 --- /dev/null +++ b/qa/rpc-tests/feature_issuance.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016 The Bitcoin Core developers +# Distributed under the MIT/X11 software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +" Tests issued assets functionality including (re)issuance, and de-issuance " + +# Creates a raw issuance transaction based on the passed in list, checking important details after +def process_raw_issuance(node, issuance_list): + if len(issuance_list) > 5: + raise Exception('Issuance list too long') + # Make enough outputs for any subsequent spend + next_destinations = {} + output_values = (node.getbalance()['bitcoin']-1)/5 + for i in range(5): + next_destinations[node.getnewaddress()] = output_values + + raw_tx = node.createrawtransaction([], next_destinations) + funded_tx = node.fundrawtransaction(raw_tx)['hex'] + issued_call_details = node.rawissueasset(funded_tx, issuance_list) + issued_tx = issued_call_details[-1]["hex"] # Get hex from end + # don't accept blinding fail, and blind all issuances or none at all + blind_tx = node.blindrawtransaction(issued_tx, False, [], issuance_list[0]["blind"]) + signed_tx = node.signrawtransaction(blind_tx) + tx_id = node.sendrawtransaction(signed_tx['hex']) + node.generate(1) + assert_equal(node.gettransaction(tx_id)["confirmations"], 1) + num_issuance = 0 + decoded_tx = node.decoderawtransaction(signed_tx['hex']) + decoded_unblind_tx = node.decoderawtransaction(issued_tx) + for i, (issuance_req, tx_input, issuance_result) in enumerate(zip(issuance_list, decoded_tx["vin"], issued_call_details)): + if "issuance" not in tx_input: + break + + num_issuance += 1 + issuance_details = tx_input["issuance"] + if "blind" not in issuance_req or issuance_req["blind"] == True: + + assert("assetamount" not in issuance_details) + assert("tokenamount" not in issuance_details) + assert_equal(issuance_details["assetBlindingNonce"], "00"*32) + if "asset_amount" in issuance_req: + assert("assetamountcommitment" in issuance_details) + if "token_amount" in issuance_req: + assert("tokenamountcommitment" in issuance_details) + else: + if "asset_amount" in issuance_req: + assert_equal(issuance_details["assetamount"], issuance_req["asset_amount"]) + if "token_amount" in issuance_req: + assert_equal(issuance_details["tokenamount"], issuance_req["token_amount"]) + + # Cross-check RPC call result details with raw details + assert_equal(issuance_result["vin"], i) + assert_equal(issuance_result["entropy"], issuance_details["assetEntropy"]) + if "asset" in issuance_details: + assert_equal(issuance_result["asset"], issuance_details["asset"]) + if "token" in issuance_details: + assert_equal(issuance_result["token"], issuance_details["token"]) + + # Look for outputs assets where we expect them, or not, initial issuance first then token + for issuance_type in ["asset", "token"]: + blind_dest = issuance_type+"_address" not in issuance_req or node.validateaddress(issuance_req[issuance_type+"_address"])["confidential_key"] != "" + if blind_dest: + # We should not find any the issuances we made since the addresses confidential + for output in decoded_tx["vout"]: + if "asset" in output and output["asset"] == issuance_details[issuance_type]: + raise Exception("Found asset in plaintext that should be confidential!") + + # Now scan unblinded version of issuance outputs + asset_found = False + for output in decoded_unblind_tx["vout"]: + if "asset" in output and output["asset"] == issuance_details[issuance_type]: + if issuance_type+"_address" not in issuance_req: + raise Exception("Found asset type not requested") + if "value" in output and \ + output["value"] == issuance_req[issuance_type+"_amount"]: + asset_found = True + + # Find the asset type if it was created + assert(asset_found if issuance_type+"_address" in issuance_req else True) + + assert_equal(num_issuance, len(issuance_list)) + +class IssuanceTest (BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.num_nodes = 3 + self.setup_clean_chain = True + + def setup_network(self, split=False): + self.nodes = start_nodes(self.num_nodes, self.options.tmpdir) + connect_nodes_bi(self.nodes,0,1) + connect_nodes_bi(self.nodes,1,2) + self.is_network_split=False + self.sync_all() + + def run_test(self): + + # Unblinded issuance of asset + issued = self.nodes[0].issueasset(1, 1, False) + assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["asset"]], 1) + assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["token"]], 1) + # Quick unblinded reissuance check, making 2*COIN total + self.nodes[0].reissueasset(issued["asset"], 1) + + self.nodes[0].generate(1) + self.sync_all() + + issued2 = self.nodes[0].issueasset(2, 1) + test_asset = issued2["asset"] + assert_equal(self.nodes[0].getwalletinfo(test_asset)['balance'], Decimal(2)) + + assert_equal(self.nodes[1].getwalletinfo(test_asset)['balance'], Decimal(0)) + + # Send some bitcoin to other nodes + self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 3) + self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 3) + self.nodes[0].generate(1) + self.sync_all() + + # Destroy assets + pre_destroy_btc_balance = self.nodes[2].getwalletinfo()['balance']['bitcoin'] + self.nodes[2].destroyamount('bitcoin', 2) # Destroy 2 BTC + self.nodes[2].generate(1) + self.sync_all() + + issuedamount = self.nodes[0].getwalletinfo()['balance'][issued["token"]] + assert_equal(issuedamount, Decimal('1.0')) + self.nodes[0].destroyamount(issued["token"], issuedamount) # Destroy all reissuance tokens of one type + + self.nodes[0].generate(1) + self.sync_all() + assert(issued["token"] not in self.nodes[0].getinfo()['balance']) + + # Test various issuance and auditing paths + + issuancedata = self.nodes[0].issueasset(Decimal('0.00000002'), Decimal('0.00000001')) #2 of asset, 1 reissuance token + self.nodes[1].generate(1) + self.sync_all() + assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["asset"]], Decimal('0.00000002')) + assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000001')) + self.nodes[0].reissueasset(issuancedata["asset"], Decimal('0.00000001')) + self.sync_all() + assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["asset"]], Decimal('0.00000003')) + # Can't reissue an issuance token (yet) + try: + self.nodes[0].reissueasset(issuancedata["token"], Decimal('0.00000001')) + raise AssertionError("You shouldn't be able to reissue a token yet") + except JSONRPCException: + pass + + + issuancedata = self.nodes[2].issueasset(Decimal('0.00000005'), 0) #5 of asset, 0 reissuance token + # No reissuance tokens + try: + self.nodes[2].reissueasset(issuancedata["token"], 5) + raise AssertionError("You shouldn't be able to reissue without a token") + except JSONRPCException: + pass + + issuancedata = self.nodes[2].issueasset(0, Decimal('0.00000006')) #0 of asset, 6 reissuance token + + # Node 2 will send node 1 a reissuance token, both will generate assets + self.nodes[2].sendtoaddress(self.nodes[1].getnewaddress(), Decimal('0.00000001'), "", "", False, issuancedata["token"]) + # node 1 needs to know about a (re)issuance to reissue itself + self.nodes[1].importaddress(self.nodes[2].gettransaction(issuancedata["txid"])["details"][0]["address"]) + # also send some bitcoin + self.nodes[2].generate(1) + self.sync_all() + + assert_equal(self.nodes[2].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000005')) + assert_equal(self.nodes[1].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000001')) + redata1 = self.nodes[1].reissueasset(issuancedata["asset"], Decimal('0.05')) + redata2 = self.nodes[2].reissueasset(issuancedata["asset"], Decimal('0.025')) + + self.sync_all() + # Watch-only issuances won't show up in wallet until confirmed + self.nodes[1].generate(1) + self.sync_all() + + # Now have node 0 audit these issuances + blindingkey1 = self.nodes[1].dumpissuanceblindingkey(redata1["txid"], redata1["vin"]) + blindingkey2 = self.nodes[2].dumpissuanceblindingkey(redata2["txid"], redata2["vin"]) + blindingkey3 = self.nodes[2].dumpissuanceblindingkey(issuancedata["txid"], issuancedata["vin"]) + + # Need addr to get transactions in wallet. TODO: importissuances? + txdet1 = self.nodes[1].gettransaction(redata1["txid"])["details"] + txdet2 = self.nodes[2].gettransaction(redata2["txid"])["details"] + txdet3 = self.nodes[2].gettransaction(issuancedata["txid"])["details"] + + # Receive addresses added last + addr1 = txdet1[len(txdet1)-1]["address"] + addr2 = txdet2[len(txdet2)-1]["address"] + addr3 = txdet3[len(txdet3)-1]["address"] + + assert_equal(len(self.nodes[0].listissuances()), 6); + self.nodes[0].importaddress(addr1) + self.nodes[0].importaddress(addr2) + self.nodes[0].importaddress(addr3) + + issuances = self.nodes[0].listissuances() + assert_equal(len(issuances), 9) + + for issue in issuances: + if issue['txid'] == redata1["txid"] and issue['vin'] == redata1["vin"]: + assert_equal(issue['assetamount'], Decimal('-1')) + if issue['txid'] == redata2["txid"] and issue['vin'] == redata2["vin"]: + assert_equal(issue['assetamount'], Decimal('-1')) + if issue['txid'] == issuancedata["txid"] and issue['vin'] == issuancedata["vin"]: + assert_equal(issue['assetamount'], Decimal('-1')) + assert_equal(issue['tokenamount'], Decimal('-1')) + + # Test that importing the issuance blinding keys then reveals the issuance amounts + self.nodes[0].importissuanceblindingkey(redata1["txid"], redata1["vin"], blindingkey1) + self.nodes[0].importissuanceblindingkey(redata2["txid"], redata2["vin"], blindingkey2) + self.nodes[0].importissuanceblindingkey(issuancedata["txid"], issuancedata["vin"], blindingkey3) + + issuances = self.nodes[0].listissuances() + + for issue in issuances: + if issue['txid'] == redata1["txid"] and issue['vin'] == redata1["vin"]: + assert_equal(issue['assetamount'], Decimal('0.05')) + if issue['txid'] == redata2["txid"] and issue['vin'] == redata2["vin"]: + assert_equal(issue['assetamount'], Decimal('0.025')) + if issue['txid'] == issuancedata["txid"] and issue['vin'] == issuancedata["vin"]: + assert_equal(issue['assetamount'], Decimal('0')) + assert_equal(issue['tokenamount'], Decimal('0.00000006')) + + # Check for value accounting when asset issuance is null but token not, ie unblinded + issued = self.nodes[0].issueasset(0, 1, False) + assert(issued["asset"] not in self.nodes[0].getwalletinfo()["balance"]) + assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["token"]], 1) + + + print("Raw issuance tests") + # Addresses to send to to check proper blinding + blind_addr = self.nodes[0].getnewaddress() + nonblind_addr = self.nodes[0].validateaddress(blind_addr)['unconfidential'] + + # Fail making non-witness issuance sourcing a single unblinded output. + # See: https://github.com/ElementsProject/elements/issues/473 + total_amount = self.nodes[0].getbalance()['bitcoin'] + self.nodes[0].sendtoaddress(nonblind_addr, total_amount, "", "", True) + self.nodes[1].generate(1) + raw_tx = self.nodes[0].createrawtransaction([], {nonblind_addr:self.nodes[0].getbalance()['bitcoin']-1}) + funded_tx = self.nodes[0].fundrawtransaction(raw_tx)['hex'] + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr}])[0]["hex"] + blind_tx = self.nodes[0].blindrawtransaction(issued_tx) + signed_tx = self.nodes[0].signrawtransaction(blind_tx) + assert_raises_jsonrpc(-26, "", self.nodes[0].sendrawtransaction, signed_tx['hex']) + + # Make single blinded output to ensure we work around above issue + total_amount = self.nodes[0].getbalance()['bitcoin'] + self.nodes[0].sendtoaddress(blind_addr, total_amount, "", "", True) + self.nodes[1].generate(1) + + # Start with single issuance input, unblinded (makes 5 outputs for later larger issuances) + process_raw_issuance(self.nodes[0], [{"asset_amount":2, "asset_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":2, "asset_address":nonblind_addr, "blind":True}]) + process_raw_issuance(self.nodes[0], [{"token_amount":5, "token_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":nonblind_addr, "token_amount":2, "token_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":nonblind_addr, "token_amount":2, "token_address":blind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":blind_addr, "token_amount":2, "token_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":blind_addr, "token_amount":2, "token_address":blind_addr, "blind":False}]) + # Now do multiple with some issuance outputs blind, some unblinded + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":nonblind_addr, "token_amount":2, "token_address":nonblind_addr, "blind":False}, {"asset_amount":2, "asset_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":blind_addr, "token_amount":2, "token_address":nonblind_addr, "blind":False}, {"asset_amount":2, "asset_address":nonblind_addr, "blind":False}]) + # Up to 5 issuances since we're making 5 outputs each time + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":nonblind_addr, "token_amount":2, "token_address":blind_addr, "blind":False}, {"asset_amount":2, "asset_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":1, "asset_address":nonblind_addr, "token_amount":2, "token_address":blind_addr, "blind":False}, {"asset_amount":3, "asset_address":nonblind_addr, "blind":False}, {"asset_amount":4, "asset_address":nonblind_addr, "token_amount":5, "token_address":blind_addr, "blind":False}, {"asset_amount":6, "asset_address":nonblind_addr, "token_amount":7, "token_address":blind_addr, "blind":False}, {"asset_amount":8, "asset_address":nonblind_addr, "token_amount":9, "token_address":blind_addr, "blind":False}]) + # Default "blind" value is true, ommitting explicit argument for last + process_raw_issuance(self.nodes[0], [{"asset_amount":1, "asset_address":nonblind_addr, "token_amount":2, "token_address":blind_addr, "blind":True}, {"asset_amount":3, "asset_address":nonblind_addr, "blind":True}, {"asset_amount":4, "asset_address":nonblind_addr, "token_amount":5, "token_address":blind_addr, "blind":True}, {"asset_amount":6, "asset_address":nonblind_addr, "token_amount":7, "token_address":blind_addr, "blind":True}, {"asset_amount":8, "asset_address":nonblind_addr, "token_amount":9, "token_address":blind_addr}]) + + # Make sure contract hash is being interpreted as expected, resulting in different asset ids + raw_tx = self.nodes[0].createrawtransaction([], {nonblind_addr:self.nodes[0].getbalance()['bitcoin']-1}) + funded_tx = self.nodes[0].fundrawtransaction(raw_tx)['hex'] + id_set = set() + + # First issue an asset with no argument + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + + # Again with 00..00 argument, which match the no-argument case + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "contract_hash":"00"*32}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + assert_equal(len(id_set), 1) + + # Random contract string should again differ + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "contract_hash":"deadbeef"*8}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + assert_equal(len(id_set), 2) + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "contract_hash":"deadbeee"*8}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + assert_equal(len(id_set), 3) + + # Finally, append an issuance on top of an already-"issued" raw tx + # Same contract, different utxo being spent results in new asset type + issued_tx = self.nodes[2].rawissueasset(issued_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "contract":"deadbeee"*8}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][1]["issuance"]["asset"]) + assert_equal(len(id_set), 4) + # This issuance should not have changed + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + assert_equal(len(id_set), 4) + +if __name__ == '__main__': + IssuanceTest ().main () diff --git a/src/blind.cpp b/src/blind.cpp index 703b0f0bd7d..ebc9cd2ed6a 100644 --- a/src/blind.cpp +++ b/src/blind.cpp @@ -174,12 +174,28 @@ void CreateValueCommitment(CConfidentialValue& confValue, secp256k1_pedersen_com assert(confValue.IsValid()); } +size_t GetNumIssuances(const CMutableTransaction& tx) +{ + unsigned int numIssuances = 0; + for (unsigned int i = 0; i < tx.vin.size(); i++) { + if (!tx.vin[i].assetIssuance.IsNull()) { + if (!tx.vin[i].assetIssuance.nAmount.IsNull()) { + numIssuances++; + } + if (!tx.vin[i].assetIssuance.nInflationKeys.IsNull()) { + numIssuances++; + } + } + } + return numIssuances; +} + int BlindTransaction(std::vector& input_blinding_factors, const std::vector& input_asset_blinding_factors, const std::vector& input_assets, const std::vector& input_amounts, std::vector& output_blinding_factors, std::vector& output_asset_blinding_factors, const std::vector& output_pubkeys, const std::vector& vBlindIssuanceAsset, const std::vector& vBlindIssuanceToken, CMutableTransaction& tx, std::vector >* auxiliary_generators) { // Sanity check input data and output_pubkey size, clear other output data assert(tx.vout.size() >= output_pubkeys.size()); - assert(tx.vin.size() >= vBlindIssuanceAsset.size()); - assert(tx.vin.size() >= vBlindIssuanceToken.size()); + assert(tx.vin.size()+GetNumIssuances(tx) >= vBlindIssuanceAsset.size()); + assert(tx.vin.size()+GetNumIssuances(tx) >= vBlindIssuanceToken.size()); output_blinding_factors.clear(); output_blinding_factors.resize(tx.vout.size()); output_asset_blinding_factors.clear(); @@ -250,7 +266,7 @@ int BlindTransaction(std::vector& input_blinding_factors, const std::v // New Issuance if (issuance.assetBlindingNonce.IsNull()) { bool assetToBlind = (vBlindIssuanceAsset.size() > i && vBlindIssuanceAsset[i].IsValid()) ? true : false; - GenerateAssetEntropy(entropy, tx.vin[0].prevout, issuance.assetEntropy); + GenerateAssetEntropy(entropy, tx.vin[i].prevout, issuance.assetEntropy); CalculateAsset(asset, entropy); CalculateReissuanceToken(token, entropy, assetToBlind); } else { diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index e030e7d0302..bf3b04efbec 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -141,6 +141,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "testproposedblock", 1, "acceptnonstd" }, { "sendtomainchain", 2, "subtractfeefromamount"}, { "getnewblockhex", 0, "required_age"}, + { "rawissueasset", 1, "issuances"}, // Echo with conversion (For testing only) { "echojson", 0, "arg0" }, { "echojson", 1, "arg1" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index b26bbd85217..bf21a1f421e 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -712,14 +712,16 @@ void FillBlinds(CMutableTransaction& tx, bool fUseWallet, std::vector& CScript blindingScript = CScript() << OP_RETURN << std::vector(tx.vin[nIn].prevout.hash.begin(), tx.vin[nIn].prevout.hash.end()) << tx.vin[nIn].prevout.n; for (size_t nPseudo = 0; nPseudo < 2; nPseudo++) { - CConfidentialValue& confValue = (nPseudo == 0) ? issuance.nAmount : issuance.nInflationKeys; + bool issuance_asset = (nPseudo == 0); + std::vector& issuance_blinding_keys = issuance_asset ? asset_keys : token_keys; + CConfidentialValue& confValue = issuance_asset ? issuance.nAmount : issuance.nInflationKeys; if (confValue.IsCommitment()) { // Rangeproof must exist if (tx.wit.vtxinwit.size() <= nIn) { throw JSONRPCError(RPC_INVALID_PARAMETER, string("Transaction issuance is already blinded but has no attached rangeproof.")); } CTxInWitness& txinwit = tx.wit.vtxinwit[nIn]; - std::vector& vchRangeproof = (nPseudo == 0) ? txinwit.vchIssuanceAmountRangeproof : txinwit.vchInflationKeysRangeproof; + std::vector& vchRangeproof = issuance_asset ? txinwit.vchIssuanceAmountRangeproof : txinwit.vchInflationKeysRangeproof; uint256 blinding_factor; uint256 asset_blinding_factor; CAmount amount; @@ -728,29 +730,29 @@ void FillBlinds(CMutableTransaction& tx, bool fUseWallet, std::vector& // Wipe out confidential info from issuance vchRangeproof.clear(); confValue = CConfidentialValue(amount); - // One key both blinded values, single key needed for issuance reveal - asset_keys.push_back(pwalletMain->GetBlindingKey(&blindingScript)); - token_keys.push_back(pwalletMain->GetBlindingKey(&blindingScript)); + // One key blinds both values, single key needed for issuance reveal + issuance_blinding_keys.push_back(pwalletMain->GetBlindingKey(&blindingScript)); continue; } #endif // If no wallet, or unable to unblind, leave it alone in next blinding step - asset_keys.push_back(CKey()); - token_keys.push_back(CKey()); - } else { + issuance_blinding_keys.push_back(CKey()); + + } else if (confValue.IsExplicit()) { // Without wallet, nothing to be done. - asset_keys.push_back(CKey()); - token_keys.push_back(CKey()); + issuance_blinding_keys.push_back(CKey()); #ifdef ENABLE_WALLET // Use wallet to generate blindingkey used directly as nonce // as user is not "sending" to anyone. // Always assumed we want to blind here. // TODO Signal intent for all blinding via API including replacing nonce commitment if (fUseWallet) { - asset_keys[asset_keys.size()-1] = pwalletMain->GetBlindingKey(&blindingScript); - token_keys[token_keys.size()-1] = pwalletMain->GetBlindingKey(&blindingScript); + issuance_blinding_keys[issuance_blinding_keys.size()-1] = pwalletMain->GetBlindingKey(&blindingScript); } #endif + } else { + // Null or invalid, don't try anything but append an empty key + issuance_blinding_keys.push_back(CKey()); } } } @@ -766,6 +768,7 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) "The input raw transaction cannot have already-blinded outputs.\n" "The output keys used can be specified by using a confidential address in createrawtransaction.\n" "If an additional blinded output is required to make a balanced blinding, a 0-value unspendable output will be added. Since there is no access to the wallet the blinding pubkey from the last output with blinding key will be repeated.\n" + "You can not blind issuances with this call.\n" "\nArguments:\n" "1. \"hexstring\", (string, required) A hex-encoded raw transaction.\n" @@ -903,9 +906,9 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) #ifdef ENABLE_WALLET UniValue blindrawtransaction(const JSONRPCRequest& request) { - if (request.fHelp || (request.params.size() < 1 || request.params.size() > 4)) + if (request.fHelp || (request.params.size() < 1 || request.params.size() > 5)) throw runtime_error( - "blindrawtransaction \"hexstring\" ( ignoreblindfail [\"assetcommitment,...\"] totalblinder )\n" + "blindrawtransaction \"hexstring\" ( ignoreblindfail [\"assetcommitment,...\"] blind_issuances \"totalblinder\" )\n" "\nConvert one or more outputs of a raw transaction into confidential ones using only wallet inputs.\n" "Returns the hex-encoded raw transaction.\n" "The output keys used can be specified by using a confidential address in createrawtransaction.\n" @@ -914,11 +917,13 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) "\nArguments:\n" "1. \"hexstring\", (string, required) A hex-encoded raw transaction.\n" "2. \"ignoreblindfail\"\" (bool, optional, default=true) Return a transaction even when a blinding attempt fails due to number of blinded inputs/outputs.\n" - "3. [ (array, optional) An array of input asset generators. If provided, this list must be empty, or match the final input commitment list, including ordering, to make a valid surjection proof. This list does not include generators for issuances, as these assets are inherently unblinded.\n" + "3. \"asset_commitments\" \n" + " [ (array, optional) An array of input asset generators. If provided, this list must be empty, or match the final input commitment list, including ordering, to make a valid surjection proof. This list does not include generators for issuances, as these assets are inherently unblinded.\n" " \"assetcommitment\" (string, optional) A hex-encoded asset commitment, one for each input.\n" " Null commitments must be \"\".\n" " ],\n" - "4. \"totalblinder\" (string, optional) Ignored for now.\n" + "4. \"blind_issuances\" (bool, optional, default=true) Blind the issuances found in the raw transaction or not. All issuances will be blinded if true. \n" + "5. \"totalblinder\" (string, optional) Ignored for now.\n" "\nResult:\n" "\"transaction\" (string) hex string of the transaction\n" @@ -930,8 +935,10 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)); } else if (request.params.size() == 3){ RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)); + } else if (request.params.size() == 4){ + RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)(UniValue::VBOOL)); } else { - RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)(UniValue::VSTR)); + RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)(UniValue::VBOOL)(UniValue::VSTR)); } vector txData(ParseHexV(request.params[0], "argument 1")); @@ -966,8 +973,11 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) } } + bool blind_issuances = request.params[3].isNull() || request.params[3].get_bool(); + LOCK(pwalletMain->cs_wallet); + std::vector input_blinds; std::vector input_asset_blinds; std::vector input_assets; @@ -976,6 +986,7 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) std::vector output_asset_blinds; std::vector output_assets; std::vector output_pubkeys; + std::vector blind_issuance_asset; int n_blinded_ins = 0; for (size_t nIn = 0; nIn < tx.vin.size(); nIn++) { @@ -1014,8 +1025,14 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) std::vector asset_keys; std::vector token_keys; + // This fills out issuance blinding data for you from the wallet itself FillBlinds(tx, true, output_blinds, output_asset_blinds, output_pubkeys, asset_keys, token_keys); + if (!blind_issuances) { + asset_keys.clear(); + token_keys.clear(); + } + // How many are we trying to blind? int numPubKeys = 0; unsigned int keyIndex = 0; @@ -1026,6 +1043,12 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) keyIndex = i; } } + for (const auto& key : asset_keys) { + if (key.IsValid()) numPubKeys++; + } + for (const auto& key : token_keys) { + if (key.IsValid()) numPubKeys++; + } if (numPubKeys == 0 && n_blinded_ins == 0) { // Vacuous, just return the transaction @@ -1046,7 +1069,7 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) } } - if (BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_blinds, output_asset_blinds, output_pubkeys, std::vector(), std::vector(), tx, (auxiliary_generators.size() ? &auxiliary_generators : NULL)) != numPubKeys) { + if (BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_blinds, output_asset_blinds, output_pubkeys, asset_keys, token_keys, tx, (auxiliary_generators.size() ? &auxiliary_generators : NULL)) != numPubKeys) { // TODO Have more rich return values, communicating to user what has been blinded // User may be ok not blinding something that for instance has no corresponding type on input throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unable to blind transaction: Are you sure each asset type to blind is represented in the inputs?")); @@ -1556,6 +1579,198 @@ UniValue sendrawtransaction(const JSONRPCRequest& request) return hashTx.GetHex(); } +struct IssuanceDetails +{ + int input_index; + uint256 entropy; + CAsset asset; + CAsset token; +}; + +// Appends a single issuance to the first input that doesn't have one, and includes +// a single output per asset type in shuffled positions. +void issueasset_base(CMutableTransaction& mtx, IssuanceDetails& issuance_details, const CAmount asset_amount, const CAmount token_amount, const std::string& asset_address_str, const std::string& token_address_str, const bool blind_issuance, const uint256& contract_hash) +{ + + CBitcoinAddress asset_address(asset_address_str); + CBitcoinAddress token_address(token_address_str); + CScript asset_destination = GetScriptForDestination(asset_address.Get()); + CScript token_destination = GetScriptForDestination(token_address.Get()); + + // Find an input with no issuance field + size_t issuance_input_index = 0; + for (; issuance_input_index < mtx.vin.size(); issuance_input_index++) { + if (mtx.vin[issuance_input_index].assetIssuance.IsNull()) { + break; + } + } + // Can't add another one, exit + if (issuance_input_index == mtx.vin.size()) { + issuance_details.input_index = -1; + return; + } + + uint256 entropy; + CAsset asset; + CAsset token; + GenerateAssetEntropy(entropy, mtx.vin[issuance_input_index].prevout, contract_hash); + CalculateAsset(asset, entropy); + CalculateReissuanceToken(token, entropy, blind_issuance); + + issuance_details.input_index = issuance_input_index; + issuance_details.entropy = entropy; + issuance_details.asset = asset; + issuance_details.token = token; + + mtx.vin[issuance_input_index].assetIssuance.assetEntropy = contract_hash; + + // Place assets into randomly placed output slots, just insert in place + // -1 due to fee output being at the end no matter what. + int asset_place = GetRandInt(mtx.vout.size()-1); + int token_place = GetRandInt(mtx.vout.size()); // Don't bias insertion + + CTxOut asset_out(asset, asset_amount, asset_destination); + // If blinded address, insert the pubkey into the nonce field for later substitution by blinding + if (asset_address.IsBlinded()) { + CPubKey asset_blind = asset_address.GetBlindingKey(); + asset_out.nNonce.vchCommitment = std::vector(asset_blind.begin(), asset_blind.end()); + } + // Don't issue stuff or set values unless non-zero (both are against consensus) + if (asset_amount > 0) { + mtx.vout.insert(mtx.vout.begin()+asset_place, asset_out); + mtx.vin[issuance_input_index].assetIssuance.nAmount = asset_amount; + } + + CTxOut token_out(token, token_amount, token_destination); + // If blinded address, insert the pubkey into the nonce field for later substitution by blinding + if (token_address.IsBlinded()) { + CPubKey token_blind = token_address.GetBlindingKey(); + token_out.nNonce.vchCommitment = std::vector(token_blind.begin(), token_blind.end()); + } + // Don't issue stuff or set values unless non-zero (both are against consensus) + if (token_amount > 0) { + mtx.vout.insert(mtx.vout.begin()+token_place, token_out); + mtx.vin[issuance_input_index].assetIssuance.nInflationKeys = token_amount; + } +} + +UniValue rawissueasset(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 2) + throw runtime_error( + "rawissueasset transaction [{\"asset_amount\":x.xxx, \"asset_address\":\"address\", \"token_amount\":x.xxx, \"token_address\":\"address\", \"blind\":bool, ( \"contract_hash\":\"hash\" )}, ...]\n" + "\nCreate an asset by attaching issuances to transaction inputs. Returns the transaction hex. There must be as many inputs as issuances requested. The final transaction hex is the final version of the transaction appended to the last object in the array.\n" + "\nArguments:\n" + "1. \"transaction\" (string, required) Transaction in hex in which to include a peg-in input.\n" + "2. \"issuances\" (list, required) List of issuances to create. Each issuance must have one non-zero amount. \n" + "[\n" + " {\n" + " \"asset_amount\":x.xxx (numeric or string, optional) Amount of asset to generate, if any.\n" + " \"asset_address\":addr (string, optional) Destination address of generated asset. Required if `asset_amount` given.\n" + " \"token_amount\":x.xxx (numeric or string, optional) Amount of reissuance token to generate, if any.\n" + " \"token_address\":addr (string, optional) Destination address of generated reissuance tokens. Required if `token_amount` given.\n" + " \"blind\":bool (bool, optional, default=true) Whether to mark the issuance input for blinding or not. Only affects issuances with re-issuance tokens." + " \"contract_hash\":str (string, optional, default=00..00) Contract hash that is put into issuance definition. Must be 32 bytes worth in hex string form. This will affect the asset id." + " }\n" + " ...\n" + "]\n" + "\nResult:\n" + "[ (json array) Results of issuances, in the order of `issuances` argument\n" + " { (json object)\n" + " \"hex\":, (string) The transaction with issuances appended. Only appended to final index in returned array.\n" + " \"vin\":\"n\", (numeric) The input position of the issuance in the transaction.\n" + " \"entropy\":\"\" (string) Entropy of the asset type.\n" + " \"asset\":\"\", (string) Asset type for issuance if known.\n" + " \"token\":\"\", (string) Token type for issuance.\n" + " },\n" + " ...\n" + "]" + ); + + CMutableTransaction mtx; + + if (!DecodeHexTx(mtx, request.params[0].get_str())) + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + + UniValue issuances = request.params[1].get_array(); + + std::string asset_address_str = ""; + std::string token_address_str = ""; + + UniValue ret(UniValue::VARR); + + // Count issuances, only append hex to final one + unsigned int issuances_til_now = 0; + + for (unsigned int idx = 0; idx < issuances.size(); idx++) { + const UniValue& issuance = issuances[idx]; + const UniValue& issuance_o = issuance.get_obj(); + + CAmount asset_amount = 0; + const UniValue& asset_amount_uni = issuance_o["asset_amount"]; + if (asset_amount_uni.isNum()) { + asset_amount = AmountFromValue(asset_amount_uni); + if (asset_amount <= 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, asset_amount must be positive"); + } + const UniValue& asset_address_uni = issuance_o["asset_address"]; + if (!asset_address_uni.isStr()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing corresponding asset_address"); + } + asset_address_str = asset_address_uni.get_str(); + } + + CAmount token_amount = 0; + const UniValue& token_amount_uni = issuance_o["token_amount"]; + if (token_amount_uni.isNum()) { + token_amount = AmountFromValue(token_amount_uni); + if (token_amount <= 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, token_amount must be positive"); + } + const UniValue& token_address_uni = issuance_o["token_address"]; + if (!token_address_uni.isStr()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing corresponding token_address"); + } + token_address_str = token_address_uni.get_str(); + } + if (asset_amount == 0 && token_amount == 0) { + throw JSONRPCError(RPC_TYPE_ERROR, "Issuance must have one non-zero component"); + } + + // If we have issuances, check if reissuance tokens will be generated via blinding path + const UniValue blind_uni = issuance_o["blind"]; + const bool blind_issuance = !blind_uni.isBool() || blind_uni.get_bool(); + + // Check for optional contract to hash into definition + uint256 contract_hash; + if (!issuance_o["contract_hash"].isNull()) { + contract_hash = ParseHashV(issuance_o["contract_hash"], "contract_hash"); + } + + IssuanceDetails details; + + issueasset_base(mtx, details, asset_amount, token_amount, asset_address_str, token_address_str, blind_issuance, contract_hash); + if (details.input_index == -1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Failed to find enough blank inputs for listed issuances."); + } + + issuances_til_now++; + + UniValue obj(UniValue::VOBJ); + if (issuances_til_now == issuances.size()) { + obj.pushKV("hex", EncodeHexTx(mtx, RPCSerializationFlags())); + } + obj.pushKV("vin", details.input_index); + obj.pushKV("entropy", details.entropy.GetHex()); + obj.pushKV("asset", details.asset.GetHex()); + obj.pushKV("token", details.token.GetHex()); + + ret.push_back(obj); + } + + return ret; +} + static const CRPCCommand commands[] = { // category name actor (function) okSafeMode // --------------------- ------------------------ ----------------------- ---------- @@ -1566,8 +1781,9 @@ static const CRPCCommand commands[] = { "rawtransactions", "sendrawtransaction", &sendrawtransaction, false, {"hexstring","allowhighfees"} }, { "rawtransactions", "signrawtransaction", &signrawtransaction, false, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */ { "rawtransactions", "rawblindrawtransaction", &rawblindrawtransaction, false, {}}, + { "rawtransactions", "rawissueasset", &rawissueasset, false, {"transaction", "issuances"}}, #ifdef ENABLE_WALLET - { "rawtransactions", "blindrawtransaction", &blindrawtransaction, true, {}}, + { "rawtransactions", "blindrawtransaction", &blindrawtransaction, true, {"hexstring", "ignoreblindfail", "asset_commitments", "blind_issuances", "totalblinder"}}, #endif { "blockchain", "gettxoutproof", &gettxoutproof, true, {"txids", "blockhash"} }, { "blockchain", "verifytxoutproof", &verifytxoutproof, true, {"proof"} }, diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 5807fa48bf7..b69eabbd531 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3802,8 +3802,8 @@ UniValue issueasset(const JSONRPCRequest& request) "{ (json object)\n" " \"txid\":\"\", (string) Transaction id for issuance.\n" " \"vin\":\"n\", (numeric) The input position of the issuance in the transaction.\n" - " \"entropy\":\"\" (string) Entropy of the asset type.\n" - " \"asset\":\"\", (string) Asset type for issuance if known.\n" + " \"entropy\":\"\", (string) Entropy of the asset type.\n" + " \"asset\":\"\", (string) Asset type for issuance.\n" " \"token\":\"\", (string) Token type for issuance.\n" "}\n" "\nExamples:\n"