diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 8e1a49c652..ab2b13b880 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -229,6 +229,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendtoaddress", 10, "ignoreblindfail" }, { "createrawtransaction", 4, "output_assets" }, { "createpsbt", 4, "output_assets" }, + { "calculateasset", 1, "vout" }, + { "calculateasset", 3, "blind_reissuance" }, }; // clang-format on diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 622b2e6fb4..6dcaaf2f37 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -3026,6 +3026,56 @@ static RPCHelpMan rawreissueasset() }; } +static RPCHelpMan calculateasset() +{ + return RPCHelpMan{"calculateasset", + "\nCalculate the asset tags and reissuance asset tags for a given prevout and contract hash\n", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Transaction id of the output that will be spent for this issuance."}, + {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "Output index of the output that will be spent for this issuance."}, + {"asset_entropy", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "Additional asset entropy to be included in the asset tag. This is the contract hash."}, + {"blind_reissuance", RPCArg::Type::STR_HEX, /* default */ "true", "Whether the reissuance asset tag will be blinded"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "asset_tag", "Calculated asset tag."}, + {RPCResult::Type::STR_HEX, "reissuance_asset_tag", "Asset tag for the reissuance tokens."}, + {RPCResult::Type::STR_HEX, "final_asset_entropy", "The calculated asset entropy that is needed for reissuance."}, + }, + }, + RPCExamples{""}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VNUM, UniValue::VSTR, UniValue::VBOOL}, true); + + uint256 txid = ParseHashV(request.params[0], "txid"); + uint32_t vout = request.params[1].get_int(); + + uint256 asset_entropy; + if (!request.params[2].isNull()) { + asset_entropy = ParseHashV(request.params[2], "asset_entropy"); + } + + bool blind_reissuance = request.params[3].isNull() ? true : request.params[3].get_bool(); + + uint256 entropy; + CAsset asset; + CAsset token; + COutPoint outpoint(txid, vout); + GenerateAssetEntropy(entropy, outpoint, asset_entropy); + CalculateAsset(asset, entropy); + CalculateReissuanceToken(token, entropy, blind_reissuance); + + UniValue out(UniValue::VOBJ); + out.pushKV("asset_tag", asset.GetHex()); + out.pushKV("reissuance_asset_tag", token.GetHex()); + out.pushKV("final_asset_entropy", entropy.GetHex()); + return out; +}, + }; +} + // END ELEMENTS // @@ -3058,6 +3108,7 @@ static const CRPCCommand commands[] = { "rawtransactions", "rawissueasset", &rawissueasset, {"transaction","issuances"}}, { "rawtransactions", "rawreissueasset", &rawreissueasset, {"transaction","reissuances"}}, { "rawtransactions", "rawblindrawtransaction", &rawblindrawtransaction, {"hexstring","inputamountblinders","inputamounts","inputassets","inputassetblinders","totalblinder","ignoreblindfail"} }, + { "rawtransactions", "calculateasset", &calculateasset, {"txid", "vout", "asset_entropy", "blind_reissuance"} }, }; // clang-format on for (const auto& c : commands) { diff --git a/test/functional/feature_issuance.py b/test/functional/feature_issuance.py index a6b0e211dd..8559e452dd 100755 --- a/test/functional/feature_issuance.py +++ b/test/functional/feature_issuance.py @@ -324,22 +324,34 @@ class IssuanceTest(BitcoinTestFramework): 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"]) + calc_asset = self.nodes[0].calculateasset(decode_tx["vin"][0]["txid"], decode_tx["vin"][0]["vout"]) + assert_equal(calc_asset["final_asset_entropy"], decode_tx["vin"][0]["issuance"]["assetEntropy"]) + assert_equal(calc_asset["asset_tag"], 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) + calc_asset = self.nodes[0].calculateasset(decode_tx["vin"][0]["txid"], decode_tx["vin"][0]["vout"], "00"*32) + assert_equal(calc_asset["final_asset_entropy"], decode_tx["vin"][0]["issuance"]["assetEntropy"]) + assert_equal(calc_asset["asset_tag"], decode_tx["vin"][0]["issuance"]["asset"]) # 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) + calc_asset = self.nodes[0].calculateasset(decode_tx["vin"][0]["txid"], decode_tx["vin"][0]["vout"], "deadbeef"*8) + assert_equal(calc_asset["final_asset_entropy"], decode_tx["vin"][0]["issuance"]["assetEntropy"]) + assert_equal(calc_asset["asset_tag"], decode_tx["vin"][0]["issuance"]["asset"]) 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) + calc_asset = self.nodes[0].calculateasset(decode_tx["vin"][0]["txid"], decode_tx["vin"][0]["vout"], "deadbeee"*8) + assert_equal(calc_asset["final_asset_entropy"], decode_tx["vin"][0]["issuance"]["assetEntropy"]) + assert_equal(calc_asset["asset_tag"], decode_tx["vin"][0]["issuance"]["asset"]) # Finally, append an issuance on top of an already-"issued" raw tx # Same contract, different utxo being spent results in new asset type @@ -348,6 +360,10 @@ class IssuanceTest(BitcoinTestFramework): decode_tx = self.nodes[0].decoderawtransaction(issued_tx) id_set.add(decode_tx["vin"][1]["issuance"]["asset"]) assert_equal(len(id_set), 4) + calc_asset = self.nodes[0].calculateasset(decode_tx["vin"][1]["txid"], decode_tx["vin"][1]["vout"], None, False) + assert_equal(calc_asset["final_asset_entropy"], decode_tx["vin"][1]["issuance"]["assetEntropy"]) + assert_equal(calc_asset["asset_tag"], decode_tx["vin"][1]["issuance"]["asset"]) + assert_equal(calc_asset["reissuance_asset_tag"], decode_tx["vin"][1]["issuance"]["token"]) # This issuance should not have changed id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) assert_equal(len(id_set), 4)