@@ -184,12 +184,12 @@ namespace sdk {
184184 const auto policy_asset = net_params.get_policy_asset ();
185185 const Tx tx (extract ());
186186
187- auto inputs = inputs_to_json (session, std::move (details.at (" utxos" )));
188- auto outputs = outputs_to_json (session, tx);
187+ auto inputs_and_assets = inputs_to_json (session, std::move (details.at (" utxos" )));
188+ auto outputs = outputs_to_json (session, tx, inputs_and_assets. second );
189189 amount fee, fee_output;
190190 bool use_error = false ;
191191 std::string error;
192- for (const auto & txin : inputs ) {
192+ for (const auto & txin : inputs_and_assets. first ) {
193193 auto txin_error = j_str_or_empty (txin, " error" );
194194 if (txin_error.empty ()) {
195195 const auto asset_id = j_asset (net_params, txin);
@@ -217,8 +217,9 @@ namespace sdk {
217217 }
218218 // Calculated fee must match fee output for Liquid unless an error occurred
219219 GDK_RUNTIME_ASSERT (!m_is_liquid || fee == fee_output || !error.empty ());
220- nlohmann::json result = { { " transaction" , tx.to_hex () }, { " transaction_inputs" , std::move (inputs) },
221- { " transaction_outputs" , std::move (outputs) } };
220+ nlohmann::json result
221+ = { { " transaction" , tx.to_hex () }, { " transaction_inputs" , std::move (inputs_and_assets.first ) },
222+ { " transaction_outputs" , std::move (outputs) } };
222223 result[" fee" ] = m_is_liquid ? fee_output.value () : fee.value ();
223224 result[" network_fee" ] = 0 ;
224225 update_tx_info (session, tx, result);
@@ -228,8 +229,11 @@ namespace sdk {
228229 return result;
229230 }
230231
231- nlohmann::json Psbt::inputs_to_json (session_impl& session, nlohmann::json utxos) const
232+ std::pair<nlohmann::json, std::set<std::string>> Psbt::inputs_to_json (
233+ session_impl& session, nlohmann::json utxos) const
232234 {
235+ const auto & net_params = session.get_network_parameters ();
236+ std::set<std::string> wallet_assets;
233237 nlohmann::json::array_t inputs;
234238 inputs.resize (get_num_inputs ());
235239 for (size_t i = 0 ; i < inputs.size (); ++i) {
@@ -243,6 +247,7 @@ namespace sdk {
243247 utxo.erase (" user_status" );
244248 utxo_add_paths (session, utxo);
245249 input_utxo = std::move (utxo);
250+ wallet_assets.insert (j_asset (net_params, input_utxo));
246251 break ;
247252 }
248253 }
@@ -276,17 +281,20 @@ namespace sdk {
276281 input_utxo[" redeem_script" ] = b2h (redeem_script.value ());
277282 }
278283 }
279- return inputs;
284+ return std::make_pair ( std::move ( inputs), std::move (wallet_assets)) ;
280285 }
281286
282- nlohmann::json Psbt::outputs_to_json (session_impl& session, const Tx& tx) const
287+ nlohmann::json Psbt::outputs_to_json (
288+ session_impl& session, const Tx& tx, const std::set<std::string>& wallet_assets) const
283289 {
284290 const auto & net_params = session.get_network_parameters ();
291+ const bool is_electrum = net_params.is_electrum ();
292+ std::set<std::string> spent_assets;
293+ std::map<std::string, std::vector<size_t >> asset_outputs;
285294
286295 nlohmann::json::array_t outputs;
287296 outputs.resize (get_num_outputs ());
288297 for (size_t i = 0 ; i < outputs.size (); ++i) {
289- // TODO: change identification
290298 const auto & txout = get_output (i);
291299 auto & jsonout = outputs[i];
292300 if (!m_is_liquid) {
@@ -323,7 +331,8 @@ namespace sdk {
323331 jsonout[" scriptpubkey" ] = b2h ({ txout.script , txout.script_len });
324332 }
325333 auto output_data = session.get_scriptpubkey_data ({ txout.script , txout.script_len });
326- if (output_data.empty ()) {
334+ const bool is_wallet_output = !output_data.empty ();
335+ if (!is_wallet_output) {
327336 jsonout[" address" ] = get_address_from_scriptpubkey (net_params, { txout.script , txout.script_len });
328337 } else {
329338 if (m_is_liquid) {
@@ -345,6 +354,37 @@ namespace sdk {
345354 confidentialize_address (net_params, unconf_addr, jsonout.at (" blinding_key" ));
346355 jsonout[" address" ] = std::move (unconf_addr.at (" address" ));
347356 }
357+ // Change detection
358+ auto asset_id = j_asset (net_params, jsonout);
359+ if (wallet_assets.count (asset_id)) {
360+ if (!is_electrum) {
361+ // Multisig: Collect info to compute change below
362+ if (is_wallet_output) {
363+ asset_outputs[asset_id].push_back (i);
364+ } else {
365+ spent_assets.emplace (std::move (asset_id));
366+ }
367+ } else if (is_wallet_output) {
368+ // Singlesig: Outputs on the internal chain are change
369+ jsonout[" is_change" ] = j_bool_or_false (jsonout, " is_internal" );
370+ }
371+ }
372+ }
373+ if (!is_electrum) {
374+ // Multisig change detection (heuristic)
375+ for (const auto & o : asset_outputs) {
376+ if (wallet_assets.count (o.first )) {
377+ // This is an asset that we contributed an input to
378+ const bool is_spent_externally = spent_assets.count (o.first ) != 0 ;
379+ const auto num_wallet_outputs = o.second .size ();
380+ if (is_spent_externally || num_wallet_outputs > 1 ) {
381+ // We sent this asset elsewhere and also to the wallet, or
382+ // we have multiple wallet outputs for the same asset.
383+ // Mark the first (possibly only) wallet output as change.
384+ outputs[o.second .front ()][" is_change" ] = true ;
385+ }
386+ }
387+ }
348388 }
349389 return outputs;
350390 }
0 commit comments