Skip to content

Commit c9c81b9

Browse files
lklimekclaude
andcommitted
fix: case-insensitive .dash suffix and UTXO double-spend prevention
1. DPNS name normalization: strip the `.dash` suffix case-insensitively before querying Platform, so `.DASH`, `.Dash`, etc. all resolve. 2. UTXO double-spend prevention via optimistic validation: after signing the transaction (optimistically, without locks), re-acquire the write lock and verify that every selected UTXO is still in the spendable set before marking them spent. If a concurrent caller already claimed an outpoint, return an error instead of broadcasting a transaction the network would reject. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 66c3ace commit c9c81b9

File tree

2 files changed

+44
-8
lines changed
  • packages
    • rs-platform-wallet/src/wallet/core
    • rs-sdk/src/platform/dpns_usernames

2 files changed

+44
-8
lines changed

packages/rs-platform-wallet/src/wallet/core/wallet.rs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ use std::sync::Arc;
44

55
use super::balance::WalletBalance;
66

7+
use dashcore::Address as DashAddress;
78
use dashcore::secp256k1::{Message, Secp256k1};
89
use dashcore::sighash::SighashCache;
9-
use dashcore::Address as DashAddress;
1010
use dashcore::{OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
1111
use key_wallet::Utxo;
1212
use tokio::sync::RwLock;
@@ -316,6 +316,38 @@ impl CoreWallet {
316316
self.sign_transaction_inputs(&secp, &mut tx, &selected_utxos)
317317
.await?;
318318

319+
// 5b. Validate-and-mark under write lock: verify that the UTXOs we
320+
// selected (under a read lock earlier) haven't been claimed by a
321+
// concurrent caller. If they have, fail fast with a clear error
322+
// instead of broadcasting a transaction the network will reject.
323+
{
324+
use crate::wallet::platform_wallet_traits::WalletTransactionChecker;
325+
use key_wallet::transaction_checking::TransactionContext;
326+
let mut state = self.state.write().await;
327+
328+
// Re-check that our selected outpoints are still spendable.
329+
let current_spendable: std::collections::BTreeSet<OutPoint> = state
330+
.managed_state
331+
.wallet_info()
332+
.get_spendable_utxos()
333+
.iter()
334+
.map(|u| u.outpoint)
335+
.collect();
336+
337+
for (outpoint, _, _) in &selected_utxos {
338+
if !current_spendable.contains(outpoint) {
339+
return Err(PlatformWalletError::TransactionBuild(
340+
"Selected UTXOs are no longer available (concurrent transaction). Please retry.".to_string(),
341+
));
342+
}
343+
}
344+
345+
// All UTXOs still available — mark them as spent atomically.
346+
state
347+
.check_core_transaction(&tx, TransactionContext::Mempool, true, true)
348+
.await;
349+
}
350+
319351
// 6. Broadcast.
320352
self.broadcast_transaction(&tx).await?;
321353

@@ -479,12 +511,16 @@ impl CoreWallet {
479511
// Derive private keys and sign.
480512
for (i, (input, sighash)) in tx.input.iter_mut().zip(sighashes).enumerate() {
481513
let path = &derivation_paths[i];
482-
let extended_key = info.managed_state.wallet().derive_extended_private_key(path).map_err(|e| {
483-
PlatformWalletError::TransactionBuild(format!(
484-
"Failed to derive key for input {}: {}",
485-
i, e
486-
))
487-
})?;
514+
let extended_key = info
515+
.managed_state
516+
.wallet()
517+
.derive_extended_private_key(path)
518+
.map_err(|e| {
519+
PlatformWalletError::TransactionBuild(format!(
520+
"Failed to derive key for input {}: {}",
521+
i, e
522+
))
523+
})?;
488524
let input_private_key = extended_key.to_priv();
489525

490526
let message = Message::from_digest(sighash.into());

packages/rs-sdk/src/platform/dpns_usernames/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ impl Sdk {
427427
let label = if let Some(dot_pos) = name.rfind('.') {
428428
let (label_part, suffix) = name.split_at(dot_pos);
429429
// Only strip the suffix if it's exactly ".dash"
430-
if suffix == ".dash" {
430+
if suffix.eq_ignore_ascii_case(".dash") {
431431
label_part
432432
} else {
433433
// If it's not ".dash", treat the whole thing as the label

0 commit comments

Comments
 (0)