Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
b5ed6e4
feat(rs-platform-wallet): add address_derivation_info and fee_paid ac…
lklimek Apr 27, 2026
3cf4f06
docs(rs-platform-wallet): add e2e framework README
lklimek Apr 27, 2026
c7479e6
feat(rs-platform-wallet): scaffold e2e test framework skeleton
lklimek Apr 27, 2026
c3f0faa
feat(rs-platform-wallet): wave 3a — bank, factory, signer, registry, …
lklimek Apr 27, 2026
e37e60b
feat(rs-platform-wallet): implement e2e SDK, SPV, ContextProvider
lklimek Apr 27, 2026
c397a38
feat(rs-platform-wallet): wave 4 — wire harness, setup, and first e2e…
lklimek Apr 27, 2026
89d39e7
docs(rs-platform-wallet): qa wave 5 todos for follow-up
lklimek Apr 27, 2026
540bf42
fix(rs-platform-wallet): wave 6 — clear QA-001, polish under-funded p…
lklimek Apr 27, 2026
4eb879d
fix(rs-platform-wallet): use dash_async::block_on in SpvContextProvider
lklimek Apr 27, 2026
8ac22ee
fix(rs-platform-wallet): derive addr_2 only after addr_1 is observed …
lklimek Apr 27, 2026
e064044
fix(rs-platform-wallet): trim auto-selected last input to consumed am…
lklimek Apr 27, 2026
a0d50e0
refactor(rs-platform-wallet): event-driven wait_for_balance via Platf…
lklimek Apr 27, 2026
0609acf
revert(rs-platform-wallet): drop test-only production additions; abso…
lklimek Apr 27, 2026
ba90e7e
fix(rs-platform-wallet): SPV mn-list sync wait predicate (e2e framework)
lklimek Apr 27, 2026
c83bb7f
chore(rs-platform-wallet): drop dead persistence stub; document e2e a…
lklimek Apr 27, 2026
546be56
refactor(rs-platform-wallet): use TrustedHttpContextProvider; defer S…
lklimek Apr 27, 2026
276e50a
fix(rs-platform-wallet): bump e2e SWEEP_FEE_ESTIMATE to clear testnet…
lklimek Apr 27, 2026
fe454e2
fix(rs-platform-wallet): cleanup uses Explicit input selection to byp…
lklimek Apr 27, 2026
f86abce
fix(rs-platform-wallet): bump e2e SWEEP_FEE_ESTIMATE to cover multi-i…
lklimek Apr 27, 2026
b4d1a6f
chore(rs-platform-wallet): align e2e .env loading with rs-sdk; un-ign…
lklimek Apr 27, 2026
7d11975
fix(rs-platform-wallet): address Copilot review on PR #3549
lklimek Apr 27, 2026
d394a84
Merge remote-tracking branch 'origin/v3.1-dev' into feat/rs-platform-…
lklimek Apr 28, 2026
cf8260a
Merge branch 'fix/rs-platform-wallet-auto-select-inputs' into feat/rs…
lklimek Apr 28, 2026
72a9c94
docs(rs-platform-wallet/e2e): trim verbose code comments
lklimek Apr 29, 2026
4b46815
refactor(rs-platform-wallet/e2e): delegate signers to simple_signer::…
lklimek Apr 29, 2026
b882aa2
refactor(rs-platform-wallet/e2e): single parse_network helper delegat…
lklimek Apr 29, 2026
ff1a187
refactor(rs-sdk): promote address_inputs::{fetch_inputs_with_nonce,no…
lklimek Apr 29, 2026
dab9285
fix(rs-platform-wallet/e2e): scope panic-cancel to per-test, not fram…
lklimek Apr 29, 2026
8ef44a1
fix(rs-platform-wallet/e2e): drop multi_thread runtime requirement on…
lklimek Apr 29, 2026
20404b3
fix(rs-platform-wallet): reserve fee headroom at DeductFromInput(0) t…
lklimek Apr 28, 2026
86f7f04
test(rs-platform-wallet): protocol-level reproduction of CodeRabbit f…
lklimek Apr 28, 2026
da98a10
refactor(rs-platform-wallet): sort auto-select candidates by balance …
lklimek Apr 28, 2026
459a61c
fix(rs-platform-wallet): enforce min_input_amount, restrict fee_strat…
lklimek Apr 28, 2026
854ba2b
docs(rs-platform-wallet): trim verbose comments in auto_select_inputs…
lklimek Apr 29, 2026
142dfed
feat(rs-platform-wallet): support ReduceOutput(0) fee strategy in aut…
lklimek Apr 29, 2026
7b5df76
refactor(rs-platform-wallet/e2e): derive default account/key-class fr…
lklimek Apr 29, 2026
ed7308c
refactor(rs-platform-wallet/e2e): default fee strategy to ReduceOutpu…
lklimek Apr 29, 2026
559eb52
refactor(rs-platform-wallet/e2e): pin bank sweep target to address in…
lklimek Apr 29, 2026
0f4cc68
refactor(rs-platform-wallet/e2e): simplify drain_to_bank to ReduceOut…
lklimek Apr 29, 2026
6223f1e
refactor(rs-platform-wallet/e2e): per-source-type sweep helpers with …
lklimek Apr 29, 2026
af714d9
Merge branch 'fix/rs-platform-wallet-auto-select-inputs' into feat/rs…
lklimek Apr 29, 2026
096c15b
fix(simple-signer): migrate from_seed_for_identity to identity_authen…
lklimek Apr 29, 2026
5b6acd0
refactor(rs-platform-wallet/e2e): use key_wallet::DIP17_GAP_LIMIT dir…
lklimek Apr 29, 2026
0dd7ec7
refactor(rs-platform-wallet/e2e): route bank index-0 derivation throu…
lklimek Apr 29, 2026
ae98ccf
refactor(rs-platform-wallet/e2e): drop SeedBackedPlatformAddressSigne…
lklimek Apr 29, 2026
d0b1b96
Merge branch 'fix/rs-platform-wallet-auto-select-inputs' into feat/rs…
lklimek Apr 30, 2026
796f92c
fix(rs-platform-wallet/e2e): address review feedback batch + fee-tole…
lklimek Apr 30, 2026
a4696c6
refactor(rs-platform-wallet/e2e): derive testnet DAPI list from dash-…
lklimek Apr 30, 2026
95fc6c2
test(rs-platform-wallet/e2e): bump transfer fixture to dodge platform…
lklimek Apr 30, 2026
da73e21
Merge remote-tracking branch 'origin/fix/rs-platform-wallet-auto-sele…
lklimek Apr 30, 2026
eeeab5e
refactor(rs-platform-wallet/e2e): use SdkBuilder::new_testnet() now t…
lklimek Apr 30, 2026
ffe107c
refactor(rs-platform-wallet/e2e): make P2P port configurable, derive …
lklimek Apr 30, 2026
5515ba9
refactor(rs-platform-wallet/e2e): resolve all Config defaults at cons…
lklimek Apr 30, 2026
aad27c5
Merge branch 'fix/rs-platform-wallet-auto-select-inputs' into feat/rs…
lklimek May 4, 2026
59cba08
feat(platform-wallet): e2e test spec and harness extensions (#3563)
lklimek May 4, 2026
0be256a
test(rs-platform-wallet/e2e): address review feedback batch — gate li…
lklimek May 4, 2026
74b1ed7
docs(rs-platform-wallet/e2e): consolidate TEST_SPEC.md spec evolution…
lklimek May 4, 2026
8c7ec00
fix(rs-platform-wallet/e2e): bank.fund_address pays fee from input [Q…
lklimek May 5, 2026
921833f
Merge branch 'fix/rs-platform-wallet-auto-select-inputs' into feat/rs…
lklimek May 5, 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
81 changes: 63 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions packages/rs-platform-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,39 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# the non-test build keeps the leaner default-feature SDK above.
dash-sdk = { path = "../rs-sdk", default-features = false, features = ["dashpay-contract", "dpns-contract", "mocks"] }

# E2E test framework — see `tests/e2e/` for the integration harness
# that exercises the wallet → SDK → broadcast pipeline against a
# live testnet bank wallet. Pinned to the canonical published crate
# names; cargo normalizes dash/underscore in keys but the published
# name is the source of truth (e.g. `tokio-shared-rt`).
tokio-shared-rt = "0.1"
tempfile = "3"
dotenvy = "0.15"
bip39 = "2"
fs2 = "0.4"
serde = { version = "1", features = ["derive"] }
simple-signer = { path = "../simple-signer", features = ["derive"] }
parking_lot = "0.12"
# `dash-async::block_on` is the runtime-flavor-agnostic bridge used by
# `framework/context_provider.rs` to call `SpvRuntime`'s async API
# from the synchronous `ContextProvider` trait. Handles all three
# tokio runtime scenarios (no runtime, current-thread, multi-thread)
# without the `block_in_place` panic that `tokio::task::block_in_place`
# triggers on a current-thread runtime.
dash-async = { path = "../rs-dash-async" }
# `rt` feature gives us `CancellationToken` for the panic-hook +
# graceful-shutdown wiring described in the e2e plan.
tokio-util = { version = "0.7", features = ["rt"] }
# `TrustedHttpContextProvider` is the e2e harness's current default
# context provider. It backs `Sdk::set_context_provider` with the
# operator-trusted Quorum HTTP endpoint built into the crate (per
# network) so testnet / mainnet runs work without spinning up an
# SPV client. The SPV-backed provider lives in `framework/spv.rs`
# 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" }


[features]
default = ["bls", "eddsa"]
Expand Down
37 changes: 37 additions & 0 deletions packages/rs-platform-wallet/src/changeset/changeset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,36 @@ pub struct PlatformAddressChangeSet {
/// Last block height with recent address changes (compaction marker).
/// `None` means "no change".
pub last_known_recent_block: Option<u64>,
/// Lower-bound static fee estimate for the transfer that produced
/// this changeset, in credits. `0` for changesets not produced by
/// `transfer()` (e.g. sync-only changesets). See
/// [`Self::estimated_min_fee`].
pub fee: Credits,
}

impl PlatformAddressChangeSet {
/// Lower-bound static fee estimate for the transfer that produced
/// this changeset, in credits.
///
/// Returns `0` for changesets that didn't originate from a
/// `transfer()` call — e.g. sync-only changesets, or changesets
/// constructed via `Default::default()`. The value is the raw
/// `AddressFundsTransferTransition::estimate_min_fee(input_count,
/// output_count, version)` result captured at submit time — it is
/// **NOT** the actual on-chain fee and is **NOT** adjusted by the
/// `fee_strategy`.
///
/// `estimate_min_fee` only models the static
/// `state_transition_min_fees` floor; chain-time fees include
/// storage + processing costs that scale with the operation set
/// (~6.5M static vs ~14.94M observed real for 1in/1out at the time
/// of writing). Tests asserting on the actual chain-time debit
/// must read the post-broadcast balance delta directly, not this
/// value. See platform issue #3040 for the open ticket on
/// upgrading `estimate_min_fee` to a chain-time-accurate estimate.
pub fn estimated_min_fee(&self) -> Credits {
self.fee
}
}

impl Merge for PlatformAddressChangeSet {
Expand All @@ -606,13 +636,20 @@ impl Merge for PlatformAddressChangeSet {
.map_or(r, |existing| existing.max(r)),
);
}
// Fee: append-sum via `saturating_add`. Sync-only merges
// (`fee == 0`) are a no-op so a transfer's recorded fee
// survives untouched; merging two transfer changesets sums
// the per-operation fees so the merged total reflects the
// "total fee paid across operations in this batch" intent.
self.fee = self.fee.saturating_add(other.fee);
}
Comment on lines 583 to 645
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: PlatformAddressChangeSet::fee is a public Credits field that knowingly misrepresents the on-chain fee, and Merge silently sums it

pub fee: Credits stores AddressFundsTransferTransition::estimate_min_fee(...), which the accessor's own doc admits is "NOT the actual on-chain fee" — the static state_transition_min_fees floor (~6.5M for 1in/1out) is far below the real chain-time debit (~14.94M observed; see #3040). Because the field is public and shares its type with real credit values, callers reaching for cs.fee (rather than the doc-laden estimated_min_fee() accessor) get the unfiltered footgun. Merge::merge then does self.fee = self.fee.saturating_add(other.fee), so a merged changeset's reported fee is "the sum across operations of a number that already lied for one operation," compounding the misrepresentation. Pick one of: (a) wrap in a newtype like EstimatedMinFee(Credits) so the type system surfaces the caveat at every call site, (b) rename the field to static_min_fee_estimate so accidental consumers can't confuse it with paid fees, or (c) keep it pub(crate) until #3040 lands and the value can mean what callers naturally expect.

source: ['claude']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet/src/changeset/changeset.rs`:
- [SUGGESTION] lines 583-645: `PlatformAddressChangeSet::fee` is a public `Credits` field that knowingly misrepresents the on-chain fee, and `Merge` silently sums it
  `pub fee: Credits` stores `AddressFundsTransferTransition::estimate_min_fee(...)`, which the accessor's own doc admits is "NOT the actual on-chain fee" — the static `state_transition_min_fees` floor (~6.5M for 1in/1out) is far below the real chain-time debit (~14.94M observed; see #3040). Because the field is public and shares its type with real credit values, callers reaching for `cs.fee` (rather than the doc-laden `estimated_min_fee()` accessor) get the unfiltered footgun. `Merge::merge` then does `self.fee = self.fee.saturating_add(other.fee)`, so a merged changeset's reported fee is "the sum across operations of a number that already lied for one operation," compounding the misrepresentation. Pick one of: (a) wrap in a newtype like `EstimatedMinFee(Credits)` so the type system surfaces the caveat at every call site, (b) rename the field to `static_min_fee_estimate` so accidental consumers can't confuse it with paid fees, or (c) keep it `pub(crate)` until #3040 lands and the value can mean what callers naturally expect.

Comment on lines 583 to 645
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: PlatformAddressChangeSet::fee is a public Credits field whose own doc admits it does not represent the on-chain fee, and Merge silently sums it

Confirmed at HEAD. pub fee: Credits (line 589) stores AddressFundsTransferTransition::estimate_min_fee(...), which estimated_min_fee() (lines 592-611) explicitly calls out as "NOT the actual on-chain fee" and "NOT adjusted by the fee_strategy" — the static state_transition_min_fees floor (~6.5M for 1in/1out) is far below the chain-time debit (~14.94M observed; see issue #3040). Because the field is pub and shares its type with real credit values, callers reaching for cs.fee rather than the doc-laden accessor get the unfiltered footgun. Merge::merge (line 644) does self.fee = self.fee.saturating_add(other.fee), so a merged changeset's reported fee is a sum of values that already lied per-operation. Pick one of: (a) wrap in a newtype like EstimatedMinFee(Credits) so the type system surfaces the caveat at every call site; (b) rename to static_min_fee_estimate so accidental consumers can't confuse it with paid fees; or (c) keep it pub(crate) until #3040 lands.

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet/src/changeset/changeset.rs`:
- [SUGGESTION] lines 583-645: PlatformAddressChangeSet::fee is a public Credits field whose own doc admits it does not represent the on-chain fee, and Merge silently sums it
  Confirmed at HEAD. `pub fee: Credits` (line 589) stores `AddressFundsTransferTransition::estimate_min_fee(...)`, which `estimated_min_fee()` (lines 592-611) explicitly calls out as "NOT the actual on-chain fee" and "NOT adjusted by the `fee_strategy`" — the static `state_transition_min_fees` floor (~6.5M for 1in/1out) is far below the chain-time debit (~14.94M observed; see issue #3040). Because the field is `pub` and shares its type with real credit values, callers reaching for `cs.fee` rather than the doc-laden accessor get the unfiltered footgun. `Merge::merge` (line 644) does `self.fee = self.fee.saturating_add(other.fee)`, so a merged changeset's reported fee is a sum of values that already lied per-operation. Pick one of: (a) wrap in a newtype like `EstimatedMinFee(Credits)` so the type system surfaces the caveat at every call site; (b) rename to `static_min_fee_estimate` so accidental consumers can't confuse it with paid fees; or (c) keep it `pub(crate)` until #3040 lands.


fn is_empty(&self) -> bool {
self.addresses.is_empty()
&& self.sync_height.is_none()
&& self.sync_timestamp.is_none()
&& self.last_known_recent_block.is_none()
&& self.fee == 0
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,20 @@ impl IdentityManager {
.sum::<usize>()
}

/// Snapshot of every managed identity's `Identifier` across both
/// buckets. Order is unspecified — callers that need a stable
/// order should sort the returned `Vec`.
pub fn identity_ids(&self) -> Vec<Identifier> {
let mut out: Vec<Identifier> = Vec::with_capacity(self.identity_count());
out.extend(self.out_of_wallet_identities.keys().copied());
for inner in self.wallet_identities.values() {
for managed in inner.values() {
out.push(managed.identity.id());
}
}
out
}

/// `true` iff both buckets are empty.
pub fn is_empty(&self) -> bool {
self.out_of_wallet_identities.is_empty() && self.wallet_identities.is_empty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,18 @@ impl PlatformPaymentAddressProvider {
self.last_known_recent_block = result.last_known_recent_block;
}

/// Current `last_known_recent_block` watermark.
///
/// Read-only mirror of the field used by the trait
/// implementation; exposed `pub` so wallet-level helpers
/// (notably [`super::wallet::PlatformAddressWallet::sync_watermark`])
/// can return the value to callers without going through the
/// `AddressProvider` trait. Monotonic non-decreasing across
/// `sync_finished` calls.
pub fn last_known_recent_block(&self) -> u64 {
self.last_known_recent_block
}

/// Restore incremental-sync watermark from persisted state.
pub(crate) fn set_stored_sync_state(
&mut self,
Expand Down
Loading
Loading