Skip to content
Merged
Changes from all commits
Commits
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
63 changes: 61 additions & 2 deletions packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2123,11 +2123,30 @@ unsafe fn address_info_from_ffi(
let address_str = CStr::from_ptr(entry.address_base58)
.to_str()
.map_err(|e| format!("address_base58 not UTF-8: {}", e))?;
let address = dashcore::Address::from_str(address_str)
let parsed = dashcore::Address::from_str(address_str)
.map_err(|e| format!("failed to parse address '{}': {}", address_str, e))?
.require_network(network)
.map_err(|e| format!("address '{}' not on {:?}: {}", address_str, network, e))?;
let script_pubkey = address.script_pubkey();
let script_pubkey = parsed.script_pubkey();
// Re-tag with the wallet's exact network. Devnet (and regtest)
// share testnet's base58 prefixes, so `require_network` only
// VALIDATES the parse — the returned value keeps the as-parsed
// (Testnet) tag. `Address` equality and hashing include the
// network, and every runtime lookup key is built via
// `Address::from_script(script, wallet_network)`, so a
// Testnet-tagged restored key silently misses the pool's
// address-keyed maps (`get_address_info`) on a devnet wallet.
// The observable failure: outputs paying restored addresses are
// counted (`contains_script_pub_key` is script-keyed and hits)
// but never credited as UTXOs — a restored wallet permanently
// loses change returned by its own transactions. Rebuild from
// the script so the restored key is identical to runtime keys.
let address = dashcore::Address::from_script(&script_pubkey, network).map_err(|e| {
format!(
"failed to rebuild address '{}' from its script for {:?}: {}",
address_str, network, e
)
})?;
let path_str = CStr::from_ptr(entry.derivation_path)
.to_str()
.map_err(|e| format!("derivation_path not UTF-8: {}", e))?;
Expand Down Expand Up @@ -3635,6 +3654,46 @@ mod tests {
use key_wallet::mnemonic::{Language, Mnemonic};
use key_wallet::wallet::Wallet;

/// Regression: restored pool addresses must be tagged with the
/// WALLET's network, not the network the base58 string parses as.
/// Devnet shares testnet's base58 prefixes, so a devnet wallet's
/// persisted "y…" address parses as Testnet, and `require_network`
/// only validates — it keeps the as-parsed tag. `Address` equality
/// includes the network, so a Testnet-tagged restored key misses
/// every runtime lookup built via `Address::from_script(script,
/// Devnet)`: outputs paying restored addresses were matched
/// (script-keyed `contains_script_pub_key` hits) but never credited
/// as UTXOs (`get_address_info` missed) — a restored devnet wallet
/// permanently lost the change of its own transactions.
#[test]
fn restored_address_info_is_tagged_with_wallet_network() {
use std::ffi::CString;
// A valid testnet-prefixed (0x8C, "y…") P2PKH address, as a
// devnet wallet persists them.
let addr = "yMqShkrgjTRuReBGFpQr7FozEF1QcNBBYA";
let addr_c = CString::new(addr).unwrap();
let path_c = CString::new("m/44'/1'/0'/1/0").unwrap();
let entry = CoreAddressEntryFFI {
public_key: [0u8; 33],
has_public_key: false,
pool_type_tag: AddressPoolTypeTagFFI::Internal as u8,
address_index: 0,
is_used: false,
balance: 0,
address_base58: addr_c.as_ptr(),
derivation_path: path_c.as_ptr(),
};
let info = unsafe { address_info_from_ffi(&entry, Network::Devnet) }
.expect("restore must accept a testnet-prefixed string on devnet");
let runtime_key = dashcore::Address::from_script(&info.script_pubkey, Network::Devnet)
.expect("p2pkh script must convert back to an address");
assert_eq!(
info.address, runtime_key,
"restored address must be identical (network tag included) to the \
runtime `from_script` lookup key"
);
}

/// Pins the contract that an `InBlock` unresolved-asset-lock row
/// projects onto the matching BIP44 account's in-memory
/// `transactions()` map with the correct context — the precise
Expand Down
Loading