Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
24 changes: 12 additions & 12 deletions Cargo.lock

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

16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ members = [
]

[workspace.dependencies]
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "b056d07c61f8618f05082552bbb88072290d57c1" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "b056d07c61f8618f05082552bbb88072290d57c1" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "b056d07c61f8618f05082552bbb88072290d57c1" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "b056d07c61f8618f05082552bbb88072290d57c1" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "b056d07c61f8618f05082552bbb88072290d57c1" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "b056d07c61f8618f05082552bbb88072290d57c1" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "b056d07c61f8618f05082552bbb88072290d57c1" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "b056d07c61f8618f05082552bbb88072290d57c1" }
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "5a80bd71f6ba13a5055780d644f0aa724a34ca04" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "5a80bd71f6ba13a5055780d644f0aa724a34ca04" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "5a80bd71f6ba13a5055780d644f0aa724a34ca04" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "5a80bd71f6ba13a5055780d644f0aa724a34ca04" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "5a80bd71f6ba13a5055780d644f0aa724a34ca04" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "5a80bd71f6ba13a5055780d644f0aa724a34ca04" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "5a80bd71f6ba13a5055780d644f0aa724a34ca04" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "5a80bd71f6ba13a5055780d644f0aa724a34ca04" }

tokio-metrics = "0.5"

Expand Down
8 changes: 8 additions & 0 deletions packages/rs-platform-wallet-ffi/src/asset_lock/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ use std::os::raw::c_char;

/// Build an asset lock transaction via an external mnemonic resolver.
///
/// Funding is POOLED across `platform_wallet::ASSET_LOCK_FUNDING_SOURCES`:
/// coin selection draws from the union of the BIP44 and BIP32 accounts at
/// `account_index` plus every DashPay contact-receiving account, and change
/// returns to BIP44. A lock therefore no longer needs its whole amount sitting
/// in one account, and the caller no longer has to sweep accounts together
/// first. `account_index` addresses the standard families only; DashPay
/// accounts span their own indices and are pooled in regardless.
///
/// On success:
/// - `out_tx_bytes`/`out_tx_len`: serialized signed transaction
/// - `out_derivation_path`: NUL-terminated C string with the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ fn existing_asset_lock_funding(

/// Register a new asset-lock-funded identity using an external signer.
///
/// `account_index` selects which BIP44 *standard* account (by BIP44
/// account index) the asset-lock funding UTXOs are drawn from. Only
/// BIP44 standard accounts are supported today; the Swift UI is
/// expected to filter the funding picker accordingly (CoinJoin / BIP32
/// funding for new-identity registration is not yet wired through
/// `create_funded_asset_lock_proof`).
/// `account_index` addresses the *standard* families: the asset-lock
/// funding POOLS the BIP44 and BIP32 accounts at that index together
/// with every DashPay receiving account (change returns to BIP44).
/// The index does not restrict which DashPay receiving accounts
/// contribute, so the UI must not present it as an account-scoped
/// funding or privacy choice. CoinJoin funding remains drain-only and
/// is not reachable here.
///
/// # Safety
/// - `signer_handle` must be a valid, non-destroyed `*mut SignerHandle`
Expand Down
9 changes: 6 additions & 3 deletions packages/rs-platform-wallet-ffi/src/identity_top_up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,12 @@ const MIN_TOP_UP_DUFFS: u64 = 50_500;
/// with [`AssetLockFunding::FromWalletBalance`] — the same L2 orchestrator
/// (funding resolution, IS→CL fallback, asset-lock cleanup) that
/// [`platform_wallet_register_identity_with_funding_signer`] drives for
/// registration. `account_index` selects which BIP44 *standard* account
/// the asset-lock UTXOs are drawn from (only BIP44 standard accounts are
/// supported today, matching registration).
/// registration. `account_index` addresses the *standard* families: the
/// asset lock POOLS the BIP44 and BIP32 accounts at that index together
/// with every DashPay receiving account (change returns to BIP44). The
/// index does not restrict which DashPay receiving accounts contribute —
/// callers must not present this as an account-scoped funding or privacy
/// choice (matching registration).
///
/// Unlike registration this takes NO identity-key signer: the
/// `IdentityTopUp` state-transition is signed entirely by the asset lock's
Expand Down
162 changes: 150 additions & 12 deletions packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5735,22 +5735,43 @@ fn restore_unresolved_asset_lock_tx_records(
_ => TransactionContext::Mempool,
};

// Asset-lock txs are funded from a BIP44 account; that's
// the only account map the asset-lock recovery flow
// consults (`recover_asset_lock_blocking` reads
// `info.core_wallet.accounts.standard_bip44_accounts.get(
// &account_index)...transactions().get(&out_point.txid)`),
// so restoration goes through the same map. Records for
// other variants would never be reached by that lookup.
let Some(account) = wallet_info
.accounts
// A pooled asset lock can be funded ENTIRELY from a BIP32
// account or a DashPay receiving account — the builder
// deliberately skips absent source families — so
// `standard_bip44_accounts[account_index]` may not exist
// for a perfectly valid record. The recovery lookup
// (`funding_tx_record` in sync::proof) searches BIP44 and
// BIP32 by this index, CoinJoin by index, and DashPay
// receiving accounts by txid regardless of index; restore
// the synthetic record into the FIRST present family that
// lookup searches instead of dropping it — a dropped
// record leaves an already-broadcast lock stuck at
// `Broadcast` after restart, unrecoverable by any later
// promotion path.
let accounts = &mut wallet_info.accounts;
let account = if accounts
.standard_bip44_accounts
.get_mut(&rec.account_index)
else {
.contains_key(&rec.account_index)
{
accounts.standard_bip44_accounts.get_mut(&rec.account_index)
} else if accounts
.standard_bip32_accounts
.contains_key(&rec.account_index)
{
accounts.standard_bip32_accounts.get_mut(&rec.account_index)
} else if accounts.coinjoin_accounts.contains_key(&rec.account_index) {
accounts.coinjoin_accounts.get_mut(&rec.account_index)
} else {
// Any receival account works: the proof lookup scans
// them all by txid, ignoring the tracked source index.
accounts.dashpay_receival_accounts.values_mut().next()
};
let Some(account) = account else {
stats.dropped_no_account += 1;
tracing::warn!(
account_index = rec.account_index,
"load: dropping unresolved-asset-lock tx record — no matching BIP44 account"
"load: dropping unresolved-asset-lock tx record — no account in any \
family the proof lookup searches"
);
continue;
};
Expand Down Expand Up @@ -6642,6 +6663,123 @@ mod tests {
ManagedWalletInfo::from_wallet(&wallet, 0)
}

/// Same construction as `test_managed_wallet_info_with_bip44` but with a
/// single account of the given standard/DashPay `account_type` — the
/// pooled-restore tests need wallets whose ONLY account is a non-BIP44
/// family, because that's exactly the shape the pooled builder produces
/// when it skips absent source families.
fn test_managed_wallet_info_with_account(account_type: AccountType) -> ManagedWalletInfo {
let mnemonic = Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
Language::English,
)
.expect("static BIP-39 vector must parse");
let seed = mnemonic.to_seed("");
let master = ExtendedPrivKey::new_master(Network::Testnet, &seed)
.expect("master derivation must succeed");
let secp = Secp256k1::new();
let xpub = ExtendedPubKey::from_priv(&secp, &master);
let account = Account::from_xpub(None, account_type, xpub, Network::Testnet)
.expect("Account::from_xpub on a valid xpub must succeed");
let mut accounts = key_wallet::AccountCollection::new();
accounts
.insert(account)
.expect("inserting the single account must succeed");
let wallet = Wallet::new_external_signable(Network::Testnet, [0u8; 32], accounts);
ManagedWalletInfo::from_wallet(&wallet, 0)
}

/// A lock funded EXCLUSIVELY from a BIP32 account (the pooled builder
/// skips absent families, so no BIP44 account exists at the index) must
/// restore into `standard_bip32_accounts` — the pre-fix bridge only
/// consulted BIP44 and dropped the record, leaving an already-broadcast
/// lock unrecoverable after restart.
#[test]
fn restore_routes_to_bip32_when_indexed_bip44_absent() {
let mut wallet_info = test_managed_wallet_info_with_account(AccountType::Standard {
index: 7,
standard_account_type: StandardAccountType::BIP32Account,
});
let tx = synthetic_minimal_tx();
let txid = tx.txid();
let mut tx_buf: Vec<u8> = serialize(&tx);

let rec = UnresolvedAssetLockTxRecordFFI {
account_index: 7,
tx_bytes: tx_buf.as_mut_ptr(),
tx_bytes_len: tx_buf.len(),
context_raw: 2,
block_height: 1475917,
block_hash: [0x42u8; 32],
block_timestamp: 1700000000,
first_seen: 1699999000,
};

let stats = restore_unresolved_asset_lock_tx_records(&mut wallet_info, &[rec])
.expect("restoration should not error");
assert_eq!(
stats.restored, 1,
"the BIP32-only record must restore, not drop"
);
assert!(
wallet_info
.accounts
.standard_bip32_accounts
.get(&7)
.expect("BIP32 account 7 must exist")
.transactions()
.contains_key(&txid),
"the restored record must land in the BIP32 account the proof lookup searches"
);
drop(tx_buf);
}

/// A lock funded EXCLUSIVELY from a DashPay receiving account (no
/// standard account exists at the tracked index at all) must restore
/// into a receival account — the proof lookup scans them by txid,
/// ignoring the tracked source index, so any receival account makes the
/// record findable again after restart.
#[test]
fn restore_routes_to_dashpay_receival_when_indexed_standard_absent() {
let mut wallet_info =
test_managed_wallet_info_with_account(AccountType::DashpayReceivingFunds {
index: 2,
user_identity_id: [0x11u8; 32],
friend_identity_id: [0x22u8; 32],
});
let tx = synthetic_minimal_tx();
let txid = tx.txid();
let mut tx_buf: Vec<u8> = serialize(&tx);

let rec = UnresolvedAssetLockTxRecordFFI {
// Tracked source index that matches NO standard account.
account_index: 3,
tx_bytes: tx_buf.as_mut_ptr(),
tx_bytes_len: tx_buf.len(),
context_raw: 2,
block_height: 1475917,
block_hash: [0x42u8; 32],
block_timestamp: 1700000000,
first_seen: 1699999000,
};

let stats = restore_unresolved_asset_lock_tx_records(&mut wallet_info, &[rec])
.expect("restoration should not error");
assert_eq!(
stats.restored, 1,
"the DashPay-only record must restore, not drop"
);
assert!(
wallet_info
.accounts
.dashpay_receival_accounts
.values()
.any(|account| account.transactions().contains_key(&txid)),
"the restored record must land in a receival account (searched by txid)"
);
drop(tx_buf);
}

/// Same reproducible testnet xpub as `test_managed_wallet_info_with_bip44`,
/// wrapped as a `ProviderOwnerKeys` account so the managed collection
/// ends up with a `provider_owner_keys` account carrying its address
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ use crate::{unwrap_option_or_return, unwrap_result_or_return};
/// → consume), with the asset-lock signature produced by an external
/// `MnemonicResolverHandle`.
///
/// `account_index` selects the BIP44 *standard* Core account whose
/// UTXOs fund the asset lock (only BIP44 standard accounts supported
/// today). `platform_account_index` selects which platform-payment
/// account the recipient addresses belong to.
/// `account_index` addresses the *standard* Core families: the asset
/// lock POOLS the BIP44 and BIP32 accounts at that index together with
/// every DashPay receiving account (change returns to BIP44); the index
/// does not restrict which DashPay receiving accounts contribute.
/// `platform_account_index` selects which platform-payment account the
/// recipient addresses belong to.
///
/// # Safety
/// - `signer_address_handle` must be a valid, non-destroyed
Expand Down
7 changes: 5 additions & 2 deletions packages/rs-platform-wallet-ffi/src/shielded_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -896,8 +896,11 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_shield(
/// by a `MnemonicResolverHandle` — the raw key never crosses the
/// FFI boundary.
///
/// `account_index` selects the BIP44 Core account whose UTXOs
/// fund the asset lock. `amount_duffs` is the L1 amount to lock.
/// `account_index` addresses the standard Core families: the asset
/// lock POOLS the BIP44 and BIP32 accounts at that index together
/// with every DashPay receiving account (change returns to BIP44);
/// the index does not restrict which DashPay receiving accounts
/// contribute. `amount_duffs` is the L1 amount to lock.
/// The wallet derives the shielded credit amount internally
/// (`lock_value − pool_fee`, where `pool_fee = shielded fee +
/// asset_lock_base_cost`) — callers don't need to know about
Expand Down
Loading
Loading