@@ -6,7 +6,7 @@ use crate::types::{FFINetwork, Network};
66use crate :: { check_ptr, unwrap_option_or_return, unwrap_result_or_return} ;
77use dashcore:: blockdata:: transaction:: special_transaction:: TransactionPayload ;
88use dashcore:: hashes:: Hash ;
9- use dashcore:: { Address as DashAddress , OutPoint , Txid } ;
9+ use dashcore:: { Address as DashAddress , OutPoint , TxOut , Txid } ;
1010use key_wallet:: account:: ManagedAccountCollection ;
1111use key_wallet:: managed_account:: ManagedCoreFundsAccount ;
1212use key_wallet:: wallet:: managed_wallet_info:: coin_selection:: SelectionStrategy ;
@@ -215,6 +215,24 @@ pub unsafe extern "C" fn core_wallet_tx_builder_finalize(
215215/// `core_wallet_transaction_free`). `out_bytes_ptr`/`out_bytes_len` borrow
216216/// `out_tx`'s buffer — copy them out before freeing `out_tx`.
217217///
218+ /// Value of the sole non-OP_RETURN output: what a broadcast of this
219+ /// transaction actually pays out.
220+ ///
221+ /// Returns 0 when there is no single such output. A multi-recipient build has
222+ /// no one deliverable amount, and an OP_RETURN-only build pays no one — hosts
223+ /// read the 0 as "not applicable" rather than "pays nothing", so the two cases
224+ /// need not be told apart here.
225+ ///
226+ /// Output ORDER is deliberately irrelevant: a MAYAChain deposit carries its
227+ /// memo at VOUT1, while other layouts put the data carrier first.
228+ fn sole_deliverable_value ( outputs : & [ TxOut ] ) -> u64 {
229+ let mut carriers = outputs. iter ( ) . filter ( |out| !out. script_pubkey . is_op_return ( ) ) ;
230+ match ( carriers. next ( ) , carriers. next ( ) ) {
231+ ( Some ( only) , None ) => only. value ,
232+ _ => 0 ,
233+ }
234+ }
235+
218236/// Also writes `out_deliverable_duffs`: the value of the sole non-OP_RETURN
219237/// output of the REGISTERED transaction — what a later broadcast actually
220238/// pays out. Hosts need it for a drain (`SelectionStrategy::All`), where the
@@ -358,17 +376,7 @@ pub unsafe extern "C" fn core_wallet_signed_payment_finalize(
358376 // pay. Defined only for a single-destination payment: exactly one output
359377 // that is not an OP_RETURN data carrier. Anything else reports 0, which the
360378 // host reads as "not applicable" rather than "pays nothing".
361- let deliverable_duffs = {
362- let mut carriers = finalized
363- . transaction ( )
364- . output
365- . iter ( )
366- . filter ( |out| !out. script_pubkey . is_op_return ( ) ) ;
367- match ( carriers. next ( ) , carriers. next ( ) ) {
368- ( Some ( only) , None ) => only. value ,
369- _ => 0 ,
370- }
371- } ;
379+ let deliverable_duffs = sole_deliverable_value ( & finalized. transaction ( ) . output ) ;
372380 unsafe { * out_deliverable_duffs = deliverable_duffs } ;
373381
374382 let serialized = dashcore:: consensus:: serialize ( finalized. transaction ( ) ) ;
@@ -852,3 +860,83 @@ pub unsafe extern "C" fn core_wallet_transaction_free(tx: *mut FFICoreTransactio
852860 tx. tx_bytes = std:: ptr:: null_mut ( ) ;
853861 tx. tx_len = 0 ;
854862}
863+
864+
865+ #[ cfg( test) ]
866+ mod tests {
867+ use super :: sole_deliverable_value;
868+ use dashcore:: blockdata:: script:: ScriptBuf ;
869+ use dashcore:: TxOut ;
870+
871+ /// A spendable output. The script only has to NOT be an OP_RETURN.
872+ fn destination ( value : u64 ) -> TxOut {
873+ TxOut {
874+ value,
875+ script_pubkey : ScriptBuf :: from ( vec ! [ 0x76 , 0xa9 , 0x14 ] ) ,
876+ }
877+ }
878+
879+ fn op_return ( payload : & [ u8 ] ) -> TxOut {
880+ let data = dashcore:: script:: PushBytesBuf :: try_from ( payload. to_vec ( ) )
881+ . expect ( "test payload is within push limits" ) ;
882+ TxOut {
883+ value : 0 ,
884+ script_pubkey : ScriptBuf :: new_op_return ( & data) ,
885+ }
886+ }
887+
888+ #[ test]
889+ fn a_lone_destination_is_the_deliverable_amount ( ) {
890+ assert_eq ! ( sole_deliverable_value( & [ destination( 27_442_985 ) ] ) , 27_442_985 ) ;
891+ }
892+
893+ /// The MAYAChain shape: vault output plus a zero-value memo. The memo must
894+ /// not be mistaken for a second recipient, in EITHER order — Maya puts the
895+ /// memo at VOUT1, but nothing in the calculation may depend on that.
896+ #[ test]
897+ fn a_data_carrier_beside_the_destination_is_ignored_in_both_orders ( ) {
898+ let memo = op_return ( b"=:MAYA.CACAO:maya1abc" ) ;
899+ assert_eq ! (
900+ sole_deliverable_value( & [ destination( 27_442_985 ) , memo. clone( ) ] ) ,
901+ 27_442_985 ,
902+ "memo after the destination (the Maya layout)"
903+ ) ;
904+ assert_eq ! (
905+ sole_deliverable_value( & [ memo, destination( 27_442_985 ) ] ) ,
906+ 27_442_985 ,
907+ "memo before the destination"
908+ ) ;
909+ }
910+
911+ /// Two recipients have no single deliverable amount. Reporting either one
912+ /// would let a host quote a number the payment does not pay.
913+ #[ test]
914+ fn two_spendable_outputs_report_zero ( ) {
915+ assert_eq ! ( sole_deliverable_value( & [ destination( 1_000 ) , destination( 2_000 ) ] ) , 0 ) ;
916+ }
917+
918+ #[ test]
919+ fn two_spendable_outputs_report_zero_even_beside_a_data_carrier ( ) {
920+ assert_eq ! (
921+ sole_deliverable_value( & [ destination( 1_000 ) , op_return( b"x" ) , destination( 2_000 ) ] ) ,
922+ 0
923+ ) ;
924+ }
925+
926+ /// An OP_RETURN-only build pays no one; so does an empty output set.
927+ #[ test]
928+ fn a_transaction_with_no_spendable_output_reports_zero ( ) {
929+ assert_eq ! ( sole_deliverable_value( & [ op_return( b"data only" ) ] ) , 0 ) ;
930+ assert_eq ! ( sole_deliverable_value( & [ ] ) , 0 ) ;
931+ }
932+
933+ /// An asset lock's single output IS an OP_RETURN, so it reports 0 rather
934+ /// than its burn value. That is the intended reading: the credits go to an
935+ /// identity, not to a payee a host would quote.
936+ #[ test]
937+ fn an_op_return_carrying_value_still_reports_zero ( ) {
938+ let mut burn = op_return ( b"credits" ) ;
939+ burn. value = 500_000 ;
940+ assert_eq ! ( sole_deliverable_value( & [ burn] ) , 0 ) ;
941+ }
942+ }
0 commit comments