Skip to content

Commit 08bbd42

Browse files
lklimekclaude
andcommitted
feat(rs-platform-wallet/e2e): test-only utility helpers
- TestWallet::transfer_with_inputs (PA-002 negative variant) - TestWallet::transfer_capturing_st_bytes (PA-006) - TestRegistry::get_status (PA-004) Implements TEST_SPEC.md §4 Wave F. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d37e0fa commit 08bbd42

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

packages/rs-platform-wallet/tests/e2e/framework/registry.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ impl PersistentTestWalletRegistry {
138138
.map(|(hash, entry)| (*hash, entry.clone()))
139139
.collect()
140140
}
141+
142+
/// Status of the entry for `wallet_id`, or `None` if no entry
143+
/// exists. Cheaper than [`Self::list_orphans`] for tests that
144+
/// only need to assert on a single entry's lifecycle.
145+
pub fn get_status(&self, wallet_id: WalletSeedHash) -> Option<EntryStatus> {
146+
self.state.lock().get(&wallet_id).map(|entry| entry.status)
147+
}
141148
}
142149

143150
/// Write-temp + rename JSON persist. On Windows

packages/rs-platform-wallet/tests/e2e/framework/wallet_factory.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)