@@ -134,28 +134,18 @@ impl<B: TransactionBroadcaster + ?Sized> CoreWallet<B> {
134134 . build ( )
135135 . map_err ( |e| PlatformWalletError :: TransactionBuild ( e. to_string ( ) ) ) ?;
136136
137- // Re-validate the selected outpoints are still spendable while
138- // we still hold the write lock. The lock makes our build atomic
139- // against other callers on this handle, but external mempool /
140- // block events processed before we acquired the lock may have
141- // invalidated UTXOs that were still in the spendable set when
142- // `select_inputs` ran.
143- //
144- // We deliberately do NOT mark the inputs as spent here — that
145- // happens after a successful broadcast (see #3466 review). A
146- // failed broadcast must not leave UTXOs falsely marked spent.
137+ // Sanity-check that the builder only selected outpoints from
138+ // the same height-aware spendable set we handed to input
139+ // selection. We deliberately do NOT mark the inputs as spent here
140+ // — that happens after a successful broadcast (see #3466 review).
141+ // A failed broadcast must not leave UTXOs falsely marked spent.
147142 let selected: BTreeSet < OutPoint > =
148143 tx. input . iter ( ) . map ( |txin| txin. previous_output ) . collect ( ) ;
149- let still_spendable: BTreeSet < OutPoint > = info
150- . get_spendable_utxos ( )
151- . into_iter ( )
152- . map ( |utxo| utxo. outpoint )
153- . collect ( ) ;
154- if !selected. is_subset ( & still_spendable) {
144+ let spendable_outpoints: BTreeSet < OutPoint > =
145+ spendable. iter ( ) . map ( |utxo| utxo. outpoint ) . collect ( ) ;
146+ if !selected. is_subset ( & spendable_outpoints) {
155147 return Err ( PlatformWalletError :: TransactionBuild (
156- "Selected UTXOs are no longer available (concurrent transaction). \
157- Please retry."
158- . to_string ( ) ,
148+ "Transaction builder selected an unavailable UTXO. Please retry." . to_string ( ) ,
159149 ) ) ;
160150 }
161151
@@ -164,6 +154,11 @@ impl<B: TransactionBroadcaster + ?Sized> CoreWallet<B> {
164154
165155 // Broadcast first; if the network rejects we leave wallet state
166156 // untouched so the caller can retry without manual sync repair.
157+ // This is intentional even if the remote accepted the transaction
158+ // but the broadcast path returned an error: in that ambiguous case
159+ // later attempts may reuse the same inputs locally, but the network
160+ // rejects the duplicate spend instead of us marking UTXOs spent for
161+ // a transaction that might not have propagated.
167162 self . broadcast_transaction ( & tx) . await ?;
168163
169164 // Now that the tx is in flight, register it as a mempool transaction
@@ -175,15 +170,23 @@ impl<B: TransactionBroadcaster + ?Sized> CoreWallet<B> {
175170 // broadcast failure.
176171 {
177172 let mut wm = self . wallet_manager . write ( ) . await ;
178- let ( wallet, info) =
179- wm. get_wallet_mut_and_info_mut ( & self . wallet_id )
180- . ok_or_else ( || {
181- crate :: error:: PlatformWalletError :: WalletNotFound (
182- "Wallet not found in wallet manager" . to_string ( ) ,
183- )
184- } ) ?;
185- info. check_core_transaction ( & tx, TransactionContext :: Mempool , wallet, true , true )
186- . await ;
173+ if let Some ( ( wallet, info) ) = wm. get_wallet_mut_and_info_mut ( & self . wallet_id ) {
174+ let check_result = info
175+ . check_core_transaction ( & tx, TransactionContext :: Mempool , wallet, true , true )
176+ . await ;
177+ if !check_result. is_relevant {
178+ tracing:: warn!(
179+ txid = %tx. txid( ) ,
180+ "broadcast transaction was not relevant during post-broadcast wallet registration"
181+ ) ;
182+ }
183+ } else {
184+ tracing:: warn!(
185+ wallet_id = %hex:: encode( self . wallet_id) ,
186+ txid = %tx. txid( ) ,
187+ "wallet missing during post-broadcast transaction registration"
188+ ) ;
189+ }
187190 }
188191
189192 Ok ( tx)
0 commit comments