@@ -195,6 +195,96 @@ impl TestWallet {
195195 . map_err ( wallet_err)
196196 }
197197
198+ /// Like [`Self::transfer`] but with an explicit input list
199+ /// (`InputSelection::Explicit`). Used by tests that need to
200+ /// drive the SDK's address-funds path without the wallet's
201+ /// `auto_select_inputs` step — typically the negative variants
202+ /// of PA-002 that probe insufficient-funds behaviour on a
203+ /// caller-chosen input set.
204+ pub async fn transfer_with_inputs (
205+ & self ,
206+ outputs : BTreeMap < PlatformAddress , Credits > ,
207+ inputs : BTreeMap < PlatformAddress , Credits > ,
208+ ) -> FrameworkResult < PlatformAddressChangeSet > {
209+ self . wallet
210+ . platform ( )
211+ . transfer (
212+ DEFAULT_ACCOUNT_INDEX ,
213+ InputSelection :: Explicit ( inputs) ,
214+ outputs,
215+ default_fee_strategy ( ) ,
216+ Some ( PlatformVersion :: latest ( ) ) ,
217+ & self . signer ,
218+ )
219+ . await
220+ . map_err ( wallet_err)
221+ }
222+
223+ /// Like [`Self::transfer_with_inputs`] but additionally returns
224+ /// the canonical bytes of an `AddressFundsTransferTransition`
225+ /// built with the same inputs / outputs / fee strategy.
226+ ///
227+ /// Used by replay-safety tests (PA-006): re-submit the captured
228+ /// bytes via `sdk.broadcast_state_transition` and assert the
229+ /// platform rejects the duplicate. The captured bytes are taken
230+ /// from a sibling build (separate nonce fetch, separate signing
231+ /// pass) — they are NOT byte-equal to the broadcast transition,
232+ /// because the production path bumps nonces independently. For
233+ /// PA-006 that's the right shape: the test confirms the second
234+ /// submission is rejected regardless of the nonce relationship
235+ /// between the two builds.
236+ pub async fn transfer_capturing_st_bytes (
237+ & self ,
238+ outputs : BTreeMap < PlatformAddress , Credits > ,
239+ inputs : BTreeMap < PlatformAddress , Credits > ,
240+ ) -> FrameworkResult < ( PlatformAddressChangeSet , Vec < u8 > ) > {
241+ use std:: collections:: BTreeSet ;
242+
243+ use dash_sdk:: platform:: FetchMany ;
244+ use dash_sdk:: query_types:: AddressInfo ;
245+ use dpp:: prelude:: AddressNonce ;
246+ use dpp:: serialization:: PlatformSerializable ;
247+ use dpp:: state_transition:: address_funds_transfer_transition:: methods:: AddressFundsTransferTransitionMethodsV0 ;
248+ use dpp:: state_transition:: address_funds_transfer_transition:: AddressFundsTransferTransition ;
249+
250+ // Sibling build for byte capture. Fetch on-chain nonces via
251+ // the public `AddressInfo::fetch_many`, bump by one to match
252+ // the SDK's `nonce_inc` convention, sign, then serialize.
253+ // The transition is NEVER broadcast — the production
254+ // `transfer_with_inputs` below does its own nonce fetch +
255+ // sign + broadcast.
256+ let address_set: BTreeSet < PlatformAddress > = inputs. keys ( ) . copied ( ) . collect ( ) ;
257+ let address_infos = AddressInfo :: fetch_many ( self . wallet . sdk ( ) , address_set)
258+ . await
259+ . map_err ( |err| FrameworkError :: Wallet ( format ! ( "nonce fetch: {err}" ) ) ) ?;
260+ let mut inputs_with_nonce: BTreeMap < PlatformAddress , ( AddressNonce , Credits ) > =
261+ BTreeMap :: new ( ) ;
262+ for ( addr, amount) in & inputs {
263+ let info = address_infos. get ( addr) . cloned ( ) . flatten ( ) . ok_or_else ( || {
264+ FrameworkError :: Wallet ( format ! ( "address {addr:?} missing from nonce response" ) )
265+ } ) ?;
266+ inputs_with_nonce. insert ( * addr, ( info. nonce + 1 , * amount) ) ;
267+ }
268+
269+ let st = AddressFundsTransferTransition :: try_from_inputs_with_signer (
270+ inputs_with_nonce,
271+ outputs. clone ( ) ,
272+ default_fee_strategy ( ) ,
273+ & self . signer ,
274+ Default :: default ( ) ,
275+ PlatformVersion :: latest ( ) ,
276+ )
277+ . await
278+ . map_err ( |err| FrameworkError :: Wallet ( format ! ( "st build: {err}" ) ) ) ?;
279+ let bytes = PlatformSerializable :: serialize_to_bytes ( & st)
280+ . map_err ( |err| FrameworkError :: Wallet ( format ! ( "st serialize: {err}" ) ) ) ?;
281+
282+ // Production transfer with the same explicit inputs. Wallet
283+ // caches + chain state advance per the canonical path.
284+ let cs = self . transfer_with_inputs ( outputs, inputs) . await ?;
285+ Ok ( ( cs, bytes) )
286+ }
287+
198288 /// Network the wallet operates against. Mirrors `wallet.sdk().network`.
199289 fn network ( & self ) -> Network {
200290 self . wallet . sdk ( ) . network
0 commit comments