diff --git a/src/ga_tx.cpp b/src/ga_tx.cpp index d8bb40f92..d8e5d2079 100644 --- a/src/ga_tx.cpp +++ b/src/ga_tx.cpp @@ -614,15 +614,28 @@ namespace sdk { // Add all outputs and compute the total amount of satoshi to be sent amount required_total{ 0 }; + uint32_t explicit_change_index = NO_CHANGE_INDEX; if (num_addressees) { + size_t addressee_index = 0; for (auto& addressee : *addressees_p) { const auto addressee_asset_id = asset_id_from_json(net_params, addressee); if (addressee_asset_id == asset_id) { - required_total += add_tx_addressee(session, net_params, result, tx, addressee); + const auto amount = add_tx_addressee(session, net_params, result, tx, addressee); + if (!json_get_value(addressee, "is_change", false)) { + required_total += amount; + } else { + if (explicit_change_index != NO_CHANGE_INDEX) { + set_tx_error(result, "Only one explicit change addressee allowed"); + break; + } + explicit_change_index = addressee_index; + } reordered_addressees.push_back(addressee); } + ++addressee_index; } } + result["change_type"][asset_id] = explicit_change_index == NO_CHANGE_INDEX ? "generated" : "explicit"; // TODO: filter per asset or assume always single asset if (manual_selection) { @@ -758,11 +771,7 @@ namespace sdk { // so compute what we can send (everything minus the // fee) and exit the loop required_total = available_total - fee; - if (is_liquid) { - set_tx_output_commitment(net_params, tx, 0, asset_id, required_total.value()); - } else { - tx->outputs[0].satoshi = required_total.value(); - } + set_tx_output_value(net_params, tx, 0, asset_id, required_total.value()); if (num_addressees == 1u) { addressees_p->at(0)["satoshi"] = required_total.value(); } @@ -818,17 +827,30 @@ namespace sdk { continue; } - // We have more than the dust amount of change. Add a change - // output to collect it, then loop again in case the amount - // this increases the fee by requires more UTXOs. - add_tx_output(net_params, result, tx, result.at("change_address").at(asset_id).at("address"), - is_liquid ? 1 : 0, asset_id == "btc" ? std::string{} : asset_id); - have_change_output = true; - change_index = tx->num_outputs - 1; - if (is_liquid && include_fee) { - std::swap(tx->outputs[fee_index], tx->outputs[change_index]); - std::swap(fee_index, change_index); + // We have more than the dust amount of change. First look for an explicit change + // output in the addressees and if present send the change there + amount::value_type change_amount = (total - required_total - fee).value(); + + if (explicit_change_index == NO_CHANGE_INDEX) { + // No explicit change output specified, add a change output using the generated change + // address + add_tx_output(net_params, result, tx, result.at("change_address").at(asset_id).at("address"), + is_liquid ? 1 : 0, asset_id == "btc" ? std::string{} : asset_id); + have_change_output = true; + change_index = tx->num_outputs - 1; + if (is_liquid && include_fee) { + std::swap(tx->outputs[fee_index], tx->outputs[change_index]); + std::swap(fee_index, change_index); + } + } else { + // Use explicit change output + set_tx_output_value(net_params, tx, explicit_change_index, asset_id, change_amount); + auto addressees = *addressees_p; + addressees[explicit_change_index]["satoshi"] = change_amount; + change_index = explicit_change_index; + have_change_output = true; } + result["have_change"][asset_id] = have_change_output; result["change_index"][asset_id] = change_index; } @@ -850,11 +872,13 @@ namespace sdk { } else { auto& change_output = tx->outputs[change_index]; change_output.satoshi = change_amount; - const uint32_t new_change_index = get_uniform_uint32_t(tx->num_outputs); - // Randomize change output - if (change_index != new_change_index) { - std::swap(tx->outputs[new_change_index], change_output); - change_index = new_change_index; + if (explicit_change_index == NO_CHANGE_INDEX) { + // Randomize change output for non-explicit change + const uint32_t new_change_index = get_uniform_uint32_t(tx->num_outputs); + if (change_index != new_change_index) { + std::swap(tx->outputs[new_change_index], change_output); + change_index = new_change_index; + } } } } diff --git a/src/transaction_utils.cpp b/src/transaction_utils.cpp index 018b5b728..6fc23b480 100644 --- a/src/transaction_utils.cpp +++ b/src/transaction_utils.cpp @@ -359,8 +359,10 @@ namespace sdk { } // Transactions with outputs below the dust threshold (except OP_RETURN) - // are not relayed by network nodes - if (!result.value("send_all", false) && satoshi.value() < session.get_dust_threshold()) { + // are not relayed by network nodes. send_all and explicit change outputs + // have amounts set to zero because they are calculated + if (!result.value("send_all", false) && !json_get_value(addressee, "is_change", false) + && satoshi.value() < session.get_dust_threshold()) { result["error"] = res::id_invalid_amount; } @@ -371,6 +373,17 @@ namespace sdk { net_params, result, tx, address, satoshi.value(), asset_id_from_json(net_params, addressee)); } + void set_tx_output_value(const network_parameters& net_params, wally_tx_ptr& tx, uint32_t index, + const std::string& asset_id, amount::value_type satoshi) + { + const bool is_liquid = net_params.is_liquid(); + if (is_liquid) { + set_tx_output_commitment(net_params, tx, index, asset_id, satoshi); + } else { + tx->outputs[index].satoshi = satoshi; + } + } + void update_tx_size_info(const wally_tx_ptr& tx, nlohmann::json& result) { const bool valid = tx->num_inputs != 0u && tx->num_outputs != 0u; @@ -439,6 +452,8 @@ namespace sdk { : false; const uint32_t change_index = have_change ? result.at("change_index").at(asset_id).get() : NO_CHANGE_INDEX; + const bool explicit_change + = have_change ? result.at("change_type").at(asset_id).get() == "explicit" : false; amount::value_type satoshi = o.satoshi; if (net_params.is_liquid()) { @@ -461,10 +476,11 @@ namespace sdk { } }; + if (is_fee) { // Nothing to do - } else if (i == change_index) { - // Insert our change meta-data for the change output + } else if (i == change_index && !explicit_change) { + // Insert our change meta-data for the generated (non-explicit) change output const auto& change_address = result.at("change_address").at(asset_id); output.insert(change_address.begin(), change_address.end()); if (net_params.is_liquid()) { @@ -477,6 +493,11 @@ namespace sdk { if (net_params.is_liquid()) { output["public_key"] = blinding_key_from_addr(address); } + if (i == change_index && explicit_change) { + // This is an explicit change output (as opposed to one generated by the gdk) + // Mark with 'is_change' + output["is_change"] = true; + } ++addressee_index; } diff --git a/src/transaction_utils.hpp b/src/transaction_utils.hpp index d30b80a50..9ae3da913 100644 --- a/src/transaction_utils.hpp +++ b/src/transaction_utils.hpp @@ -87,6 +87,9 @@ namespace sdk { amount add_tx_addressee(ga_session& session, const network_parameters& net_params, nlohmann::json& result, wally_tx_ptr& tx, nlohmann::json& addressee); + void set_tx_output_value(const network_parameters& net_params, wally_tx_ptr& tx, uint32_t index, + const std::string& asset_id, amount::value_type satoshi); + vbf_t generate_final_vbf(byte_span_t input_abfs, byte_span_t input_vbfs, uint64_span_t input_values, const std::vector& output_abfs, const std::vector& output_vbfs, uint32_t num_inputs);