Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fe6d7c1
fix(rs-sdk): case-insensitive .dash suffix in resolve_dpns_name
lklimek May 5, 2026
26f13d9
fix(rs-platform-wallet): prevent UTXO double-spend race in send_to_ad…
lklimek May 5, 2026
0d17a63
Merge branch 'v3.1-dev' into fix/dpns-case-and-utxo-race-v3.1-dev
lklimek May 5, 2026
1bd306a
fix: improve platform wallet UTXO checks and DPNS parsing (#3595)
thepastaclaw May 6, 2026
4616cba
Merge branch 'v3.1-dev' into fix/dpns-case-and-utxo-race-v3.1-dev
lklimek May 6, 2026
23d8943
fix(rs-platform-wallet): defer change-address advance until after rev…
lklimek May 6, 2026
a3a5d96
fix(rs-platform-wallet): typed ConcurrentSpendConflict variant for re…
lklimek May 6, 2026
41c9493
fix(rs-sdk): skip DPNS contract fetch when label is empty (CMT-001)
lklimek May 6, 2026
2c7e22a
docs(rs-platform-wallet): rewrite revalidation comment to match build…
lklimek May 6, 2026
97d1532
fix(rs-platform-wallet): structured event for post-broadcast !is_rele…
lklimek May 6, 2026
79843e3
test(rs-platform-wallet): broadcast ordering + rollback contract (CMT…
lklimek May 6, 2026
391768c
docs(rs-platform-wallet): tighten and deduplicate inline comments on …
lklimek May 6, 2026
43e3f9d
fix(rs-platform-wallet): defer change-address commit past broadcast (…
lklimek May 6, 2026
5d4a61b
test(rs-platform-wallet): rename broadcast pass-through tests to matc…
lklimek May 6, 2026
cc2104f
Merge remote-tracking branch 'origin/v3.1-dev' into fix/dpns-case-and…
lklimek May 8, 2026
6aa4f42
Merge branch 'v3.1-dev' into fix/dpns-case-and-utxo-race-v3.1-dev
lklimek May 8, 2026
4dd55d2
fix: close same-UTXO concurrent-selection race in send_to_addresses (…
lklimek May 8, 2026
349b95b
feat(rs-platform-wallet): attach outpoint context to ConcurrentSpendC…
lklimek May 8, 2026
6239fda
chore(rs-platform-wallet): drop CMT-NNN review tombstones from broadc…
lklimek May 8, 2026
4d204cd
fix(rs-platform-wallet): structured tracing fields on wallet-missing …
lklimek May 8, 2026
543a8dc
Merge remote-tracking branch 'origin/v3.1-dev' into fix/dpns-case-and…
lklimek May 12, 2026
0bacd25
chore: improve error type
lklimek May 12, 2026
0188fa9
chore: improve docs
lklimek May 12, 2026
9902cbd
chore: fix build
lklimek May 12, 2026
371e2c3
fix(rs-platform-wallet): restore defensive post-build UTXO revalidation
lklimek May 13, 2026
ff56c56
chore(rs-platform-wallet-ffi): use Result::is_err in group_info tests
lklimek May 13, 2026
5466501
chore: fmt
lklimek May 13, 2026
e4cf6b3
Merge remote-tracking branch 'origin/v3.1-dev' into fix/dpns-case-and…
lklimek May 14, 2026
8d4cfe8
Merge remote-tracking branch 'origin/v3.1-dev' into fix/dpns-case-and…
lklimek May 21, 2026
25bb359
fix(rs-platform-wallet-ffi): typed FFI codes for retryable errors + s…
lklimek May 21, 2026
c29b4ba
fix(platform-wallet): close UTXO race in DashPay + harden change-inde…
lklimek May 21, 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
61 changes: 57 additions & 4 deletions packages/rs-platform-wallet/src/wallet/core/broadcast.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use dashcore::{Address as DashAddress, Transaction};
use std::collections::BTreeSet;

use dashcore::{Address as DashAddress, OutPoint, Transaction};
use key_wallet::account::account_type::StandardAccountType;
use key_wallet::transaction_checking::{TransactionContext, WalletTransactionChecker};
use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface;

use crate::broadcaster::TransactionBroadcaster;
use crate::{CoreWallet, PlatformWalletError};
Expand Down Expand Up @@ -35,7 +39,6 @@ impl<B: TransactionBroadcaster + ?Sized> CoreWallet<B> {
) -> Result<Transaction, PlatformWalletError> {
use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy;
use key_wallet::wallet::managed_wallet_info::transaction_builder::TransactionBuilder;
use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface;

if outputs.is_empty() {
return Err(PlatformWalletError::TransactionBuild(
Expand Down Expand Up @@ -127,12 +130,62 @@ impl<B: TransactionBroadcaster + ?Sized> CoreWallet<B> {
)
.map_err(|e| PlatformWalletError::TransactionBuild(e.to_string()))?;

builder
let tx = builder
.build()
.map_err(|e| PlatformWalletError::TransactionBuild(e.to_string()))?
.map_err(|e| PlatformWalletError::TransactionBuild(e.to_string()))?;

// Re-validate the selected outpoints are still spendable while
// we still hold the write lock. The lock makes our build atomic
// against other callers on this handle, but external mempool /
// block events processed before we acquired the lock may have
// invalidated UTXOs that were still in the spendable set when
// `select_inputs` ran.
//
// We deliberately do NOT mark the inputs as spent here — that
// happens after a successful broadcast (see #3466 review). A
// failed broadcast must not leave UTXOs falsely marked spent.
let selected: BTreeSet<OutPoint> =
tx.input.iter().map(|txin| txin.previous_output).collect();
let still_spendable: BTreeSet<OutPoint> = info
.get_spendable_utxos()
.into_iter()
.map(|utxo| utxo.outpoint)
.collect();
if !selected.is_subset(&still_spendable) {
return Err(PlatformWalletError::TransactionBuild(
"Selected UTXOs are no longer available (concurrent transaction). \
Please retry."
.to_string(),
));
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Outdated
}
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Outdated
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Outdated
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Outdated
Comment thread
lklimek marked this conversation as resolved.
Comment thread
lklimek marked this conversation as resolved.
Comment thread
lklimek marked this conversation as resolved.
Comment thread
lklimek marked this conversation as resolved.

tx
};

// Broadcast first; if the network rejects we leave wallet state
// untouched so the caller can retry without manual sync repair.
self.broadcast_transaction(&tx).await?;
Comment thread
lklimek marked this conversation as resolved.

// Now that the tx is in flight, register it as a mempool transaction
// so subsequent callers see the inputs as spent and don't reselect
// them. The trade-off is that two callers racing between the lock
// drop above and the broadcast can both pick the same UTXOs; the
// network resolves that race exactly as it does on `v3.1-dev`
// today, but neither caller corrupts local state on a transient
// broadcast failure.
{
let mut wm = self.wallet_manager.write().await;
let (wallet, info) =
wm.get_wallet_mut_and_info_mut(&self.wallet_id)
.ok_or_else(|| {
crate::error::PlatformWalletError::WalletNotFound(
"Wallet not found in wallet manager".to_string(),
)
})?;
info.check_core_transaction(&tx, TransactionContext::Mempool, wallet, true, true)
.await;
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Outdated
}
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Outdated
Comment thread
lklimek marked this conversation as resolved.
Outdated
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Comment thread
lklimek marked this conversation as resolved.
Comment thread
lklimek marked this conversation as resolved.

Ok(tx)
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Comment thread
lklimek marked this conversation as resolved.
}
Comment thread
lklimek marked this conversation as resolved.
Outdated
}
4 changes: 2 additions & 2 deletions packages/rs-sdk/src/platform/dpns_usernames/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,8 +426,8 @@ impl Sdk {
// Handle both "alice" and "alice.dash" formats
let label = if let Some(dot_pos) = name.rfind('.') {
let (label_part, suffix) = name.split_at(dot_pos);
// Only strip the suffix if it's exactly ".dash"
if suffix == ".dash" {
// Strip ".dash" / ".DASH" / mixed case — DPNS itself is case-insensitive.
if suffix.eq_ignore_ascii_case(".dash") {
label_part
} else {
// If it's not ".dash", treat the whole thing as the label
Expand Down
Loading