Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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 @@ -49,14 +49,14 @@ members = [
]

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

# Optimize heavy crypto crates even in dev/test builds so that
# Halo 2 proof generation and verification run at near-release speed.
Expand Down
120 changes: 120 additions & 0 deletions packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,126 @@ pub unsafe extern "C" fn core_wallet_send_to_addresses(
PlatformWalletFFIResult::ok()
}

/// Sweep the entire spendable balance of the wallet's CoinJoin account
/// (BIP44 purpose 4') to `dest_address` across one or more transactions,
/// leaving no change so the account is fully emptied.
///
/// CoinJoin "mixed coins" live on a dedicated account that
/// [`core_wallet_send_to_addresses`] cannot reach (it only handles
/// standard BIP44/BIP32 accounts). Used by the DashSync → SwiftDashSDK
/// migration to recover a user's mixed coins (no longer supported) into
/// their spendable balance. Uses the same external mnemonic-resolver
/// signer model as [`core_wallet_send_to_addresses`].
///
/// The sweep is split into one or more transactions because a heavy mixer's
/// UTXO set can exceed a single transaction's relay-size limit. On success,
/// `out_txids` receives a heap buffer of `*out_count` consecutive 32-byte
/// transaction ids in **wire order** (Dash Core's internal orientation — the
/// reverse of the hex shown in block explorers), in chunk order. Free the
/// buffer with [`core_wallet_free_tx_bytes`], passing `*out_count * 32` as the
/// length. The serialized transactions themselves are not returned — the
/// caller only needs the ids (to group the resulting withdrawals).
///
/// # Safety
/// - `dest_address` must be a valid NUL-terminated C string.
/// - `core_signer_handle` must be a valid, non-destroyed
/// `*mut MnemonicResolverHandle`. Ownership is retained by the caller.
#[no_mangle]
pub unsafe extern "C" fn core_wallet_sweep_coinjoin(
handle: Handle,
account_index: u32,
dest_address: *const c_char,
core_signer_handle: *mut MnemonicResolverHandle,
out_txids: *mut *mut u8,
out_count: *mut usize,
) -> PlatformWalletFFIResult {
check_ptr!(dest_address);
check_ptr!(core_signer_handle);
check_ptr!(out_txids);
check_ptr!(out_count);

let dest_str = unwrap_result_or_return!(std::ffi::CStr::from_ptr(dest_address).to_str());
// Parse without committing to a network; the destination is validated
// against the wallet's own network inside the storage closure below so a
// wrong-network address fails before any tx is built. This is the all-funds
// sweep, so the destination script must be spendable on this chain.
let dest_unchecked = unwrap_result_or_return!(dashcore::Address::from_str(dest_str));

let signer_addr = core_signer_handle as usize;

let option = CORE_WALLET_STORAGE.with_item(handle, |wallet| {
let wallet_id = wallet.wallet_id();
let network = wallet.network();
// Reject a wrong-network destination before building/broadcasting the
// all-funds sweep — the resulting script would be unspendable on this
// chain and the sweep is irreversible. Mirrors the withdrawal FFI.
let dest = dest_unchecked
.require_network(network)
.map_err(|e| platform_wallet::PlatformWalletError::AddressOperation(e.to_string()))?;
// SAFETY: the resolver handle is pinned alive for the duration of
// this FFI call (see fn-level safety doc). The
// `MnemonicResolverCoreSigner` lives on this stack frame and is
// dropped before the function returns.
let signer = unsafe {
MnemonicResolverCoreSigner::new(
signer_addr as *mut MnemonicResolverHandle,
wallet_id,
network,
)
};
runtime().block_on(wallet.sweep_coinjoin_to_address(account_index, dest, &signer))
});
Comment thread
llbartekll marked this conversation as resolved.
let result = unwrap_option_or_return!(option);
let txs = unwrap_result_or_return!(result);

// Emit the chunks' txids as a contiguous `count * 32` byte buffer in wire
// order (`Txid::as_byte_array`) — the orientation the app records and
// groups withdrawals by. Free with `core_wallet_free_tx_bytes(ptr,
// count * 32)`. `txs` is never empty here (the core sweep errors if no
// transaction broadcast), so `out_count >= 1` on success.
use dashcore::hashes::Hash; // brings `Txid::as_byte_array` into scope
let count = txs.len();
let mut buf: Vec<u8> = Vec::with_capacity(count * 32);
for tx in &txs {
let txid = tx.txid();
buf.extend_from_slice(txid.as_byte_array());
}
let boxed = buf.into_boxed_slice();
*out_txids = Box::into_raw(boxed) as *mut u8;
*out_count = count;
PlatformWalletFFIResult::ok()
}

/// Widen the wallet's CoinJoin account (BIP44 purpose 4') address gap limit
/// to `gap_limit` and generate the addresses so SPV watches the wider window.
///
/// Used by the DashSync → SwiftDashSDK migration's one-time CoinJoin recovery
/// scan. CoinJoin mixed coins are scattered with holes wider than the default
/// gap (30), so for wallets that used CoinJoin the app widens the gap (to match
/// DashSync's 400) before starting SPV, then reverts once the coins are swept.
/// The widened limit is in-memory only and not persisted.
///
/// On success, `out_highest_index` receives the pool's highest generated
/// address index after generation. Idempotent: re-running is a cheap no-op
/// once the window is covered.
#[no_mangle]
pub unsafe extern "C" fn core_wallet_set_coinjoin_gap_limit(
handle: Handle,
account_index: u32,
gap_limit: u32,
out_highest_index: *mut u32,
) -> PlatformWalletFFIResult {
check_ptr!(out_highest_index);

let option = CORE_WALLET_STORAGE.with_item(handle, |wallet| {
runtime().block_on(wallet.set_coinjoin_gap_limit(account_index, gap_limit))
});
let result = unwrap_option_or_return!(option);
let highest = unwrap_result_or_return!(result);
*out_highest_index = highest;
PlatformWalletFFIResult::ok()
}

/// Free transaction bytes returned by `core_wallet_send_to_addresses`.
Comment thread
llbartekll marked this conversation as resolved.
Outdated
#[no_mangle]
pub unsafe extern "C" fn core_wallet_free_tx_bytes(bytes: *mut u8, len: usize) {
Expand Down
Loading
Loading