Skip to content
Merged
Show file tree
Hide file tree
Changes from 79 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
47bf919
fix(rs-platform-wallet/e2e): bank-identity bootstrap recovers from on…
lklimek May 6, 2026
66ed769
fix(rs-platform-wallet/e2e): use HIGH key for token contract deploy (…
lklimek May 6, 2026
e235766
fix(rs-platform-wallet/e2e): tradeMode in pre-programmed and group-ga…
lklimek May 6, 2026
9d89d91
fix(rs-platform-wallet/e2e): wait for chain-confirmed balance before …
lklimek May 6, 2026
8f06378
fix(rs-platform-wallet/e2e): wait for on-chain identity balance to ma…
lklimek May 6, 2026
c379265
fix(rs-platform-wallet/e2e): require N consecutive chain-confirmed ob…
lklimek May 6, 2026
89fc25b
fix(rs-platform-wallet/e2e): size DPNS-001 funding to cover dynamic-f…
lklimek May 6, 2026
2c8ebc8
fix(rs-platform-wallet/e2e): bump token-test identity funding to clea…
lklimek May 6, 2026
27245cb
fix(rs-platform-wallet/e2e): bump TK-013/14 case-local FUNDING to cle…
lklimek May 6, 2026
b79ea20
feat(rs-platform-wallet/e2e): add wait_for_data_contract_visible help…
lklimek May 6, 2026
6857c8d
fix(rs-platform-wallet/e2e): bump TK-013/14 distribution time offset …
lklimek May 6, 2026
45380d9
fix(rs-platform-wallet/e2e): add CRITICAL key to test identities and …
lklimek May 6, 2026
74ee243
fix(rs-platform-wallet/e2e): enable dpns-contract feature on context-…
lklimek May 6, 2026
262e94a
feat(rs-platform-wallet/e2e): add Platform-view wait helpers for addr…
lklimek May 6, 2026
ae966c6
fix(rs-platform-wallet/e2e): restore QA-803 dpns-contract feature on …
lklimek May 6, 2026
e2a5d7e
fix(rs-platform-wallet/e2e): wire Platform-view waits into setup_with…
lklimek May 6, 2026
a5d0138
fix(rs-platform-wallet/e2e): rewire wait_for_data_contract_visible + …
lklimek May 6, 2026
f1347c8
fix(rs-platform-wallet/e2e): id_001 expects 4 keys after CRITICAL add…
lklimek May 6, 2026
8509a63
fix(rs-platform-wallet/e2e): wait for wallet sync after transfer in i…
lklimek May 6, 2026
a26c2f7
fix(rs-platform-wallet/e2e): bump id_sweep setup funding to clear tea…
lklimek May 6, 2026
f023ecb
fix(rs-platform-wallet/e2e): register dynamically-deployed contracts …
lklimek May 6, 2026
94a8149
fix(rs-platform-wallet/e2e): wait for identity visibility on platform…
lklimek May 6, 2026
6c3e5da
feat(rs-platform-wallet/e2e): pre-flight bank balance check with acti…
lklimek May 6, 2026
307b66b
fix(rs-platform-wallet/e2e): relax MIN_BANK_CREDITS to 500M; warn at …
lklimek May 6, 2026
d1d81a3
feat(rs-platform-wallet/e2e): PLATFORM_WALLET_E2E_DISABLE_SPV env var…
lklimek May 6, 2026
11c5b4d
docs(rs-platform-wallet/e2e): INTENTIONAL comments on PA-* hard panic…
lklimek May 7, 2026
ffbcf12
feat(rs-platform-wallet/e2e): print_bank_address also prints Core fal…
lklimek May 7, 2026
5a3c1ca
fix(rs-platform-wallet/e2e): TK-013/14 sign with CRITICAL key for tok…
lklimek May 7, 2026
38f12a3
fix(rs-platform-wallet/e2e): TK-011 align with mint-on-purchase seman…
lklimek May 7, 2026
ce2181d
fix(rs-platform-wallet/e2e): bump id_001 FUNDING_CREDITS to clear tea…
lklimek May 7, 2026
fdcce11
fix(rs-platform-wallet/e2e): make sweep_platform_addresses best-effor…
lklimek May 7, 2026
7bda838
docs(rs-platform-wallet/e2e): INTENTIONAL comments on TK-001c + TK-00…
lklimek May 7, 2026
84749df
feat(rs-platform-wallet/e2e): perpetual-distribution token contract t…
lklimek May 7, 2026
337d937
test(rs-platform-wallet/e2e): wire TK-002 using setup_with_token_perp…
lklimek May 7, 2026
1d74047
feat(rs-platform-wallet/e2e): add SeedBackedIdentitySigner::inject_id…
lklimek May 7, 2026
8a09641
test(rs-platform-wallet/e2e): wire TK-001c using rotate_identity_auth…
lklimek May 7, 2026
4dc6c39
feat(rs-platform-wallet): next_unused_receive_addresses(count) access…
lklimek May 7, 2026
fd9cce7
feat(rs-platform-wallet): output_change_address override on platform-…
lklimek May 7, 2026
376794f
docs(rs-platform-wallet/e2e): add CR-004 spec — legacy BIP32 account …
lklimek May 7, 2026
e334c0f
feat(rs-platform-wallet): CR-004 — legacy BIP32 UTXO update after spe…
lklimek May 7, 2026
6ce5bae
Revert "docs(rs-platform-wallet/e2e): add CR-004 spec — legacy BIP32 …
lklimek May 7, 2026
9e8881e
fix(rs-platform-wallet/e2e): re-apply 4 fixes silently reverted by 7b…
lklimek May 7, 2026
46fb04e
fix(rs-platform-wallet/e2e): make Core-sweep teardown best-effort to …
lklimek May 7, 2026
fafa5b4
feat(rs-platform-wallet/e2e): PLATFORM_WALLET_E2E_RUN_FAILING_BY_DESI…
lklimek May 7, 2026
9da773a
fix(rs-platform-wallet/e2e): TK-013 wait for pre-programmed epoch to …
lklimek May 7, 2026
fd44ea1
fix(rs-platform-wallet/e2e): TK-002 longer wait + tolerate typed `NoC…
lklimek May 7, 2026
67e7d66
feat(rs-platform-wallet/e2e): document parallelism contract + paralle…
lklimek May 7, 2026
066f11e
fix(rs-platform-wallet/e2e): QA-V19-001 — TK-013 polls platform block…
lklimek May 7, 2026
3bcebb5
fix(rs-platform-wallet/e2e): QA-V19-003 — drop PA-005b precondition
lklimek May 7, 2026
5d896d8
fix(rs-platform-wallet/e2e): QA-V19-002 — PA-001b declare only consum…
lklimek May 7, 2026
9c7d1a7
fix(rs-platform-wallet/e2e): bank.fund_address waits for chain confir…
lklimek May 7, 2026
6c2b8f9
merge: bring parallelism contract + parallel-safe assertions into fix…
lklimek May 7, 2026
6b65c7c
merge: bring feat/...e2e (post-test-collapse) into fix/...e2e-qa-fixe…
lklimek May 7, 2026
8b042ce
merge: feat/...e2e (post-v3.1-dev sync) into fix/...e2e-qa-fixes-v1
lklimek May 7, 2026
0d2a011
Merge branch 'feat/rs-platform-wallet-e2e' into fix/rs-platform-walle…
lklimek May 7, 2026
a447a72
fix(rs-platform-wallet/e2e): TK-013 accepts InvalidTokenClaimNoCurren…
lklimek May 7, 2026
29252dc
fix(rs-platform-wallet/e2e): PA-006b balanced sums for concurrent bro…
lklimek May 7, 2026
020f7b7
fix(rs-platform-wallet/e2e): PA-001b derive (dest, change_addr) via b…
lklimek May 7, 2026
813ce11
fix(rs-platform-wallet/e2e): PA-006b assert on-chain outcome, not bro…
lklimek May 8, 2026
093f2f4
feat(rs-platform-wallet/e2e): bank-floor hard precondition gate for T…
lklimek May 8, 2026
5a80162
fix(rs-platform-wallet/e2e): cleanup actually sweeps funded wallets b…
lklimek May 8, 2026
88309a6
feat(rs-platform-wallet/e2e): bank balance cross-check post-SPV-sync …
lklimek May 8, 2026
66014ac
fix(rs-platform-wallet/e2e): sweep_orphans runs BEFORE bank-floor pan…
lklimek May 8, 2026
571a4ce
test(rs-platform-wallet): enable dash-spv keep-finalized-transaction …
lklimek May 8, 2026
69b6e18
fix(rs-platform-wallet/e2e): cross-check fires on every init, not jus…
lklimek May 8, 2026
de12237
fix(rs-platform-wallet/e2e): TK-013 match by typed variant not displa…
lklimek May 8, 2026
e147bca
feat(rs-platform-wallet/e2e): info! on bank-floor and cross-check suc…
lklimek May 8, 2026
62bc788
fix(rs-platform-wallet/e2e): PA-001b survives threads=8 (QA-V27-006)
lklimek May 8, 2026
c4653e2
docs(rs-platform-wallet/e2e): mark PA-004b/PA-009 ignored, document V…
lklimek May 8, 2026
c6ab79c
fix(rs-platform-wallet/e2e): walk up to parent-repo .env when running…
lklimek May 8, 2026
e59adda
feat(rs-platform-wallet/e2e): per-test Drop sweep + counter-driven en…
lklimek May 8, 2026
a85ec6a
fix(rs-platform-wallet/e2e): PA-003 funding accounts for setup-fee ov…
lklimek May 8, 2026
0bbfa80
docs(rs-platform-wallet/e2e): clarify PA-004b/009/010 #[ignore] cohor…
lklimek May 8, 2026
5a1e249
fix(rs-platform-wallet/e2e): promote .env load success log to info (Q…
lklimek May 8, 2026
263861a
fix(rs-platform-wallet/e2e): tolerate sub-tDASH drift on bank cross-c…
lklimek May 8, 2026
e1dfaed
fix(rs-platform-wallet/e2e): cap SetupGuard::Drop sweep with 20s time…
lklimek May 8, 2026
d1adafd
fix(rs-platform-wallet/e2e): TK-005 funding step survives threads=8 (…
lklimek May 8, 2026
56db3ad
fix(rs-platform-wallet/e2e): TK-010 gates pause/resume on chain propa…
lklimek May 8, 2026
4f39a1c
fix(rs-platform-wallet/e2e): TK-011 gates post-mint balance read on c…
lklimek May 8, 2026
e3d29c8
Merge remote-tracking branch 'origin/feat/rs-platform-wallet-e2e' int…
lklimek May 8, 2026
e7695d2
docs(rs-platform-wallet/e2e): TODO on ID-005 cross-replica gate (PR #…
lklimek May 8, 2026
d7c9f4c
test(rs-platform-wallet/e2e): split PA-001b into per-branch sub-cases…
lklimek May 8, 2026
5ac921f
refactor(rs-platform-wallet): move test-only batch-fresh accessor to …
lklimek May 8, 2026
0b1f062
test(rs-platform-wallet/e2e): split PA-005b into per-count sub-cases …
lklimek May 8, 2026
af2c147
test(rs-platform-wallet/e2e): split PA-009 into per-property sub-case…
lklimek May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/rs-platform-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,12 @@ tokio-util = { version = "0.7", features = ["rt"] }
# and `framework/context_provider.rs` and is currently disabled
# (see harness.rs) — re-enable when SPV cold-start is stable
# (Task #15).
rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider" }

rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider", features = ["dpns-contract"] }
# In-memory test runs (NoPlatformPersistence) need finalized txs retained in RAM.
# Re-declaring here enables the feature for the test target only; production
# builds pay no memory overhead. Per upstream rust-dashcore maintainer guidance.
key-wallet = { workspace = true, features = ["keep-finalized-transactions"] }
key-wallet-manager = { workspace = true, features = ["keep-finalized-transactions"] }

[features]
default = ["bls", "eddsa"]
Expand Down
14 changes: 14 additions & 0 deletions packages/rs-platform-wallet/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ pub enum PlatformWalletError {
#[error("Address operation failed: {0}")]
AddressOperation(String),

#[error(
"gap-limit exceeded: requested {requested} fresh unused addresses but only \
{available} are derivable past the current gap-limit boundary \
(highest_used={highest_used:?}, highest_generated={highest_generated:?}, \
gap_limit={gap_limit})"
)]
GapLimitExceeded {
requested: usize,
available: u32,
highest_used: Option<u32>,
highest_generated: Option<u32>,
gap_limit: u32,
},

#[error("Arithmetic overflow on Credits in {context}")]
ArithmeticOverflow { context: String },

Expand Down
22 changes: 22 additions & 0 deletions packages/rs-platform-wallet/src/wallet/core/broadcast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,18 @@ impl<B: TransactionBroadcaster + ?Sized> CoreWallet<B> {
// network resolves that race exactly as it does on `v3.1-dev`
// today, but neither caller corrupts local state on a transient
// broadcast failure.
//
// TODO(CR-004 / dash-evo-tool#845): pin that this post-broadcast
// hook actually mutates `standard_bip32_accounts` UTXO state — not
// just `standard_bip44_accounts`. The downstream
// `key_wallet::ManagedWalletInfo::check_core_transaction` routes
// standard txs through both BIP32 and BIP44 collections via
// `TransactionRouter::get_relevant_account_types`, but if a future
// routing regression skips BIP32 (the "legacy" path DET v0.9.x
// wallets still rely on), this call will silently leave UTXOs
// marked spendable on `standard_bip32_accounts[0]`. The CR-004 e2e
// test pins the contract end-to-end; this comment names the call
// site so a reviewer chasing that test failure lands here directly.
{
let mut wm = self.wallet_manager.write().await;
let (wallet, info) =
Expand All @@ -191,6 +203,16 @@ impl<B: TransactionBroadcaster + ?Sized> CoreWallet<B> {
"Wallet not found in wallet manager".to_string(),
)
})?;
tracing::debug!(
target: "platform_wallet::core::broadcast",
txid = %tx.txid(),
account_type = ?account_type,
account_index,
inputs = tx.input.len(),
outputs = tx.output.len(),
"post-broadcast: dispatching check_core_transaction(Mempool) — \
must mark consumed UTXOs spent on the matching account collection"
);
info.check_core_transaction(&tx, TransactionContext::Mempool, wallet, true, true)
.await;
}
Expand Down
192 changes: 192 additions & 0 deletions packages/rs-platform-wallet/src/wallet/platform_addresses/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,96 @@ impl PlatformAddressWallet {
Ok(cs)
}

/// Transfer credits with an explicit "change address" override.
///
/// Companion to [`Self::transfer`] that surfaces the implicit
/// "where does the residual go?" decision as a first-class
/// parameter (PA-001b).
///
/// **Override semantics**:
/// - `output_change_address: None` — straight passthrough to
/// [`Self::transfer`]; residual stays on the input addresses
/// under the existing implicit-change behaviour. Use this branch
/// when the caller does not care where the change lands.
/// - `output_change_address: Some(change_addr)` — every input is
/// spent in full, and `change_addr` is added as an extra output
/// absorbing `Σ inputs − Σ user_outputs`. The protocol's
/// `Σ inputs == Σ outputs` invariant holds because the change
/// output exactly balances the surplus.
///
/// The change branch requires [`InputSelection::Explicit`] (or
/// [`InputSelection::ExplicitWithNonces`]): the caller declares
/// the inputs and their consumption explicitly, which is the only
/// shape where "consume the entire input balance" is unambiguous
/// — the auto-selector trims to the smallest covering prefix, so
/// it has no concept of a residual to route. The map's values
/// must therefore equal the full balances the caller wants
/// consumed; the wrapper sums them and assigns the surplus to
/// `change_addr`.
///
/// Errors:
/// - [`PlatformWalletError::AddressOperation`] when the change
/// branch is requested with [`InputSelection::Auto`], when
/// `change_addr` already appears in `user_outputs` (would merge
/// silently), or when `Σ inputs ≤ Σ user_outputs` (no surplus
/// to route).
#[allow(clippy::too_many_arguments)] // mirrors `transfer`'s signature plus the change-address override; merging into a builder would obscure the additive surface PA-001b pins.
pub async fn transfer_with_change_address<S: Signer<PlatformAddress> + Send + Sync>(
&self,
account_index: u32,
input_selection: InputSelection,
user_outputs: BTreeMap<PlatformAddress, Credits>,
output_change_address: Option<PlatformAddress>,
fee_strategy: AddressFundsFeeStrategy,
platform_version: Option<&PlatformVersion>,
address_signer: &S,
) -> Result<PlatformAddressChangeSet, PlatformWalletError> {
let Some(change_addr) = output_change_address else {
return self
.transfer(
account_index,
input_selection,
user_outputs,
fee_strategy,
platform_version,
address_signer,
)
.await;
};

let (input_sum, augmented_selection) = match input_selection {
InputSelection::Explicit(ref inputs) => (
inputs.values().copied().sum::<Credits>(),
InputSelection::Explicit(inputs.clone()),
),
InputSelection::ExplicitWithNonces(ref inputs) => (
inputs.values().map(|(_n, c)| *c).sum::<Credits>(),
InputSelection::ExplicitWithNonces(inputs.clone()),
),
InputSelection::Auto => {
return Err(PlatformWalletError::AddressOperation(
"output_change_address: Some(_) requires InputSelection::Explicit \
or ExplicitWithNonces — the auto-selector trims inputs to a covering \
prefix and has no concept of a residual to route to a change address"
.to_string(),
));
}
};

let outputs_with_change =
augment_outputs_with_change(user_outputs, change_addr, input_sum)?;

self.transfer(
account_index,
augmented_selection,
outputs_with_change,
fee_strategy,
platform_version,
address_signer,
)
.await
}

/// Auto-select inputs balance-descending and dispatch to the
/// fee-strategy-specific helper. The returned map's values are
/// the **consumed amount per address** — the protocol enforces
Expand Down Expand Up @@ -879,6 +969,45 @@ fn checked_credits_add(
})
}

/// Augment `user_outputs` with an explicit change output absorbing
/// the surplus `Σ inputs − Σ user_outputs`.
///
/// Validates the three error cases that
/// [`PlatformAddressWallet::transfer_with_change_address`] needs to
/// reject before calling [`PlatformAddressWallet::transfer`]:
/// 1. `change_addr` already declared as a user output (silent merge).
/// 2. `Σ user_outputs ≥ Σ inputs` (no surplus to route).
/// 3. arithmetic overflow on the difference (defensive — every
/// realistic call sums far below `u64::MAX`).
fn augment_outputs_with_change(
mut user_outputs: BTreeMap<PlatformAddress, Credits>,
change_addr: PlatformAddress,
input_sum: Credits,
) -> Result<BTreeMap<PlatformAddress, Credits>, PlatformWalletError> {
if user_outputs.contains_key(&change_addr) {
return Err(PlatformWalletError::AddressOperation(format!(
"output_change_address {change_addr:?} already appears in user_outputs; \
the wrapper refuses to silently merge a change-output amount into a \
caller-declared output. Pick a fresh change_addr.",
)));
}
let user_output_sum: Credits = user_outputs.values().copied().sum();
if input_sum <= user_output_sum {
return Err(PlatformWalletError::AddressOperation(format!(
"output_change_address: Some(_) requires Σ inputs ({input_sum}) > \
Σ user_outputs ({user_output_sum}); no surplus to route as change. \
Drop output_change_address or grow the input map.",
)));
}
let change_amount = checked_credits_sub(
input_sum,
user_output_sum,
"augment_outputs_with_change: change_amount",
)?;
user_outputs.insert(change_addr, change_amount);
Ok(user_outputs)
}

/// Checked sub of two `Credits` values. Returns
/// [`PlatformWalletError::ArithmeticOverflow`] when the subtraction
/// would wrap. Mirrors [`checked_credits_add`] — defensive only.
Expand Down Expand Up @@ -1820,6 +1949,69 @@ mod auto_select_tests {
}
}

/// PA-001b: the change-address override must add exactly one
/// extra output absorbing `Σ inputs − Σ user_outputs`, leaving
/// `Σ inputs == Σ outputs` so the protocol's structural
/// invariant still holds.
#[test]
fn augment_outputs_with_change_adds_residual_output() {
let user_target = p2pkh(0x22);
let change_addr = p2pkh(0x33);
let user_outputs = outputs_for(user_target, 5_000_000);
let outputs =
augment_outputs_with_change(user_outputs, change_addr, 60_000_000).expect("augment");
assert_eq!(outputs.len(), 2);
assert_eq!(outputs.get(&user_target), Some(&5_000_000));
assert_eq!(
outputs.get(&change_addr),
Some(&55_000_000),
"change output must absorb exactly the surplus"
);
let output_sum: Credits = outputs.values().sum();
assert_eq!(
output_sum, 60_000_000,
"Σ outputs must equal input sum (Σ inputs == Σ outputs invariant)"
);
}

/// PA-001b: the override must reject a `change_addr` that
/// already appears in the caller's user outputs to prevent a
/// silent merge.
#[test]
fn augment_outputs_with_change_rejects_duplicate_address() {
let target = p2pkh(0x44);
let user_outputs = outputs_for(target, 5_000_000);
let err = augment_outputs_with_change(user_outputs, target, 60_000_000)
.expect_err("change_addr equal to user output must be rejected");
match err {
PlatformWalletError::AddressOperation(msg) => {
assert!(
msg.contains("already appears in user_outputs"),
"unexpected message: {msg}"
);
}
other => panic!("expected AddressOperation, got {other:?}"),
}
}

/// PA-001b: when `Σ user_outputs ≥ Σ inputs` there is no
/// surplus to route. The wrapper must reject rather than emit a
/// zero-credit (or underflowing) change output.
#[test]
fn augment_outputs_with_change_rejects_no_surplus() {
let target = p2pkh(0x55);
let change_addr = p2pkh(0x66);
let user_outputs = outputs_for(target, 60_000_000);
let err = augment_outputs_with_change(user_outputs, change_addr, 60_000_000)
.expect_err("equal sums must be rejected: nothing to route as change");
match err {
PlatformWalletError::AddressOperation(msg) => {
assert!(msg.contains("no surplus"), "unexpected message: {msg}");
}
other => panic!("expected AddressOperation, got {other:?}"),
}
}

/// End-to-end structural validation: feed the selector's output
/// to `AddressFundsTransferTransitionV0::validate_structure` to
/// confirm the transition is shape-valid under
Expand Down
Loading
Loading