Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 3 additions & 3 deletions key-wallet-ffi/FFI_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1305,14 +1305,14 @@ This function dereferences a raw pointer to FFIWallet. The caller must ensure th
#### `wallet_build_and_sign_asset_lock_transaction`

```c
wallet_build_and_sign_asset_lock_transaction(manager: *const FFIWalletManager, wallet: *const FFIWallet, account_index: u32, funding_types: *const FFIAssetLockFundingType, identity_indices: *const u32, credit_output_scripts: *const *const u8, credit_output_script_lens: *const usize, credit_output_amounts: *const u64, credit_outputs_count: usize, fee_per_kb: u64, fee_out: *mut u64, tx_bytes_out: *mut *mut u8, tx_len_out: *mut usize, private_keys_out: *mut [u8; 32], error: *mut FFIError,) -> bool
wallet_build_and_sign_asset_lock_transaction(manager: *const FFIWalletManager, wallet: *const FFIWallet, funding_sources: *const FFIAccountTypePreference, funding_sources_count: usize, account_index: u32, funding_types: *const FFIAssetLockFundingType, identity_indices: *const u32, credit_output_scripts: *const *const u8, credit_output_script_lens: *const usize, credit_output_amounts: *const u64, credit_outputs_count: usize, fee_per_kb: u64, fee_out: *mut u64, tx_bytes_out: *mut *mut u8, tx_len_out: *mut usize, private_keys_out: *mut [u8; 32], error: *mut FFIError,) -> bool
```

**Description:**
Build and sign an asset lock transaction for Core to Platform transfers. Creates a special transaction (type 8) with `AssetLockPayload` that locks Dash for Platform credits. Derives one unique private key per credit output from the specified funding account types. # Parameters - `funding_types`: Array of `credit_outputs_count` funding account types, one per credit output (registration, top-up, invitation, etc.) - `identity_indices`: Array of `credit_outputs_count` identity indices. Only used for `IdentityTopUp` entries; ignored for other funding types. - `private_keys_out`: Caller-allocated array of `credit_outputs_count` × 32-byte buffers. On success, each `private_keys_out[i]` receives the one-time private key corresponding to `credit_output_scripts[i]`. # Safety - All pointer parameters must be valid and non-null - All parallel arrays must have at least `credit_outputs_count` elements - `private_keys_out` must point to an array of `credit_outputs_count` × `[u8; 32]` buffers - Caller must free `tx_bytes_out` with `transaction_bytes_free`
Build and sign an asset lock transaction for Core to Platform transfers. Creates a special transaction (type 8) with `AssetLockPayload` that locks Dash for Platform credits. Derives one unique private key per credit output from the specified funding account types. Funding is POOLED across the caller's `funding_sources`: coin selection draws from the union of those accounts' UTXOs, so a lock no longer needs the whole amount sitting in one account. Which accounts to pool is the caller's policy — this layer applies no default and never widens the list — so a client that wants only the primary transparent balance passes a single `BIP44` source and gets exactly the pre-pooling behavior. # Parameters - `funding_sources`: Array of `funding_sources_count` accounts to fund from, in priority order. The FIRST source supplies the change address, so pass the account that should receive change first. At least one is required. A single source is strict — it fails if the wallet has no such account — while a list of two or more skips the sources this wallet has nothing for and fails only if none of them funds anything. This entry point builds a non-drain lock, so `CoinJoin` is rejected here even as the sole source: mixed funds can only back a drain. - `funding_sources_count`: Number of entries in `funding_sources`. - `account_index`: Index addressing the standard families (BIP44, BIP32, CoinJoin). DashPay sources span their own indices and ignore it. - `funding_types`: Array of `credit_outputs_count` funding account types, one per credit output (registration, top-up, invitation, etc.) - `identity_indices`: Array of `credit_outputs_count` identity indices. Only used for `IdentityTopUp` entries; ignored for other funding types. - `private_keys_out`: Caller-allocated array of `credit_outputs_count` × 32-byte buffers. On success, each `private_keys_out[i]` receives the one-time private key corresponding to `credit_output_scripts[i]`. # Safety - All pointer parameters must be valid and non-null - `funding_sources` must have at least `funding_sources_count` elements - Every `funding_sources[i].kind` must be a declared [`FFIAccountTypePreferenceKind`] discriminant (`0..=5`). Reading any other value as that enum is undefined behavior, so it cannot be rejected here — pass the generated C enum rather than a cast integer. - All other parallel arrays must have at least `credit_outputs_count` elements - `private_keys_out` must point to an array of `credit_outputs_count` × `[u8; 32]` buffers - Caller must free `tx_bytes_out` with `transaction_bytes_free`

**Safety:**
- All pointer parameters must be valid and non-null - All parallel arrays must have at least `credit_outputs_count` elements - `private_keys_out` must point to an array of `credit_outputs_count` × `[u8; 32]` buffers - Caller must free `tx_bytes_out` with `transaction_bytes_free`
- All pointer parameters must be valid and non-null - `funding_sources` must have at least `funding_sources_count` elements - Every `funding_sources[i].kind` must be a declared [`FFIAccountTypePreferenceKind`] discriminant (`0..=5`). Reading any other value as that enum is undefined behavior, so it cannot be rejected here — pass the generated C enum rather than a cast integer. - All other parallel arrays must have at least `credit_outputs_count` elements - `private_keys_out` must point to an array of `credit_outputs_count` × `[u8; 32]` buffers - Caller must free `tx_bytes_out` with `transaction_bytes_free`

**Module:** `transaction`

Expand Down
175 changes: 171 additions & 4 deletions key-wallet-ffi/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,14 +744,101 @@ impl From<FFIAssetLockFundingType> for AssetLockFundingType {
}
}

/// Which family of accounts a funding source names.
///
/// The discriminant of [`FFIAccountTypePreference`]; the two `Dashpay…` kinds
/// are the ones that read the identity IDs alongside it.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FFIAccountTypePreferenceKind {
/// The standard transparent account, `m/44'/coinType'/index'`.
BIP44 = 0,
/// The legacy standard account, `m/index'`.
BIP32 = 1,
/// Mixed CoinJoin funds. Asset locks accept these only as the *sole*
/// source, and only in drain mode — pooling mixed outputs with transparent
/// ones in one transaction links them and undoes the mixing.
///
/// `wallet_build_and_sign_asset_lock_transaction` never drains, so this
/// kind always fails there; it exists for the drain entry points.
CoinJoin = 2,
/// One contact's receiving account, named by both identity IDs.
DashpayFriendshipReceivingFunds = 3,
/// Every receiving account of one identity; reads `user_identity_id` only.
DashpayIdentityReceivingFunds = 4,
/// Every DashPay receiving account this wallet can sign for. Ignores both
/// identity ID fields.
AllDashpayReceivingFunds = 5,
}

/// A funding source offered to an asset lock's coin selection.
///
/// `kind` selects the family; the identity IDs are read only by the kinds that
/// name one (see [`FFIAccountTypePreferenceKind`]) and may be left zeroed
/// otherwise.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct FFIAccountTypePreference {
/// Which family of accounts this source names.
pub kind: FFIAccountTypePreferenceKind,
/// The local identity whose accounts to draw from. Read by
/// `DashpayFriendshipReceivingFunds` and `DashpayIdentityReceivingFunds`.
pub user_identity_id: [u8; 32],
/// The contact on the other side of the friendship. Read by
/// `DashpayFriendshipReceivingFunds` only.
pub friend_identity_id: [u8; 32],
}

impl From<FFIAccountTypePreference> for AccountTypePreference {
fn from(ffi: FFIAccountTypePreference) -> Self {
match ffi.kind {
FFIAccountTypePreferenceKind::BIP44 => Self::BIP44,
FFIAccountTypePreferenceKind::BIP32 => Self::BIP32,
FFIAccountTypePreferenceKind::CoinJoin => Self::CoinJoin,
FFIAccountTypePreferenceKind::DashpayFriendshipReceivingFunds => {
Self::DashpayFriendshipReceivingFunds {
user_identity_id: ffi.user_identity_id,
friend_identity_id: ffi.friend_identity_id,
}
}
FFIAccountTypePreferenceKind::DashpayIdentityReceivingFunds => {
Self::DashpayIdentityReceivingFunds {
user_identity_id: ffi.user_identity_id,
}
}
FFIAccountTypePreferenceKind::AllDashpayReceivingFunds => {
Self::AllDashpayReceivingFunds
}
}
}
}

/// Build and sign an asset lock transaction for Core to Platform transfers.
///
/// Creates a special transaction (type 8) with `AssetLockPayload` that locks
/// Dash for Platform credits. Derives one unique private key per credit output
/// from the specified funding account types.
///
/// Funding is POOLED across the caller's `funding_sources`: coin selection
/// draws from the union of those accounts' UTXOs, so a lock no longer needs the
/// whole amount sitting in one account. Which accounts to pool is the caller's
/// policy — this layer applies no default and never widens the list — so a
/// client that wants only the primary transparent balance passes a single
/// `BIP44` source and gets exactly the pre-pooling behavior.
///
/// # Parameters
///
/// - `funding_sources`: Array of `funding_sources_count` accounts to fund from,
/// in priority order. The FIRST source supplies the change address, so pass
/// the account that should receive change first. At least one is required.
/// A single source is strict — it fails if the wallet has no such account —
/// while a list of two or more skips the sources this wallet has nothing for
/// and fails only if none of them funds anything. This entry point builds a
/// non-drain lock, so `CoinJoin` is rejected here even as the sole source:
/// mixed funds can only back a drain.
/// - `funding_sources_count`: Number of entries in `funding_sources`.
/// - `account_index`: Index addressing the standard families (BIP44, BIP32,
/// CoinJoin). DashPay sources span their own indices and ignore it.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/// - `funding_types`: Array of `credit_outputs_count` funding account types,
/// one per credit output (registration, top-up, invitation, etc.)
/// - `identity_indices`: Array of `credit_outputs_count` identity indices.
Expand All @@ -763,13 +850,20 @@ impl From<FFIAssetLockFundingType> for AssetLockFundingType {
/// # Safety
///
/// - All pointer parameters must be valid and non-null
/// - All parallel arrays must have at least `credit_outputs_count` elements
/// - `funding_sources` must have at least `funding_sources_count` elements
/// - Every `funding_sources[i].kind` must be a declared
/// [`FFIAccountTypePreferenceKind`] discriminant (`0..=5`). Reading any other
/// value as that enum is undefined behavior, so it cannot be rejected here —
/// pass the generated C enum rather than a cast integer.
/// - All other parallel arrays must have at least `credit_outputs_count` elements
/// - `private_keys_out` must point to an array of `credit_outputs_count` × `[u8; 32]` buffers
/// - Caller must free `tx_bytes_out` with `transaction_bytes_free`
#[no_mangle]
pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction(
manager: *const FFIWalletManager,
wallet: *const FFIWallet,
funding_sources: *const FFIAccountTypePreference,
funding_sources_count: usize,
account_index: u32,
funding_types: *const FFIAssetLockFundingType,
identity_indices: *const u32,
Expand All @@ -786,6 +880,7 @@ pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction(
) -> bool {
check_ptr!(manager, error);
check_ptr!(wallet, error);
check_ptr!(funding_sources, error);
check_ptr!(funding_types, error);
check_ptr!(identity_indices, error);
check_ptr!(credit_output_scripts, error);
Expand All @@ -801,6 +896,13 @@ pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction(
return false;
}

// No implicit default: an empty list would mean `AccountTypePreference::DEFAULT`
// one layer down, which is this layer picking the caller's funding policy.
if funding_sources_count == 0 {
(*error).set(FFIErrorCode::InvalidInput, "At least one funding source required");
return false;
}

unsafe {
let manager_ref = &*manager;
let wallet_ref = &*wallet;
Expand All @@ -811,6 +913,12 @@ pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction(
let funding_types_slice = slice::from_raw_parts(funding_types, credit_outputs_count);
let identity_indices_slice = slice::from_raw_parts(identity_indices, credit_outputs_count);

let funding_sources: Vec<AccountTypePreference> =
slice::from_raw_parts(funding_sources, funding_sources_count)
.iter()
.map(|&source| source.into())
.collect();

// Convert FFI arrays to domain types
let mut fundings = Vec::with_capacity(credit_outputs_count);
for i in 0..credit_outputs_count {
Expand Down Expand Up @@ -840,9 +948,8 @@ pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction(

let result = unwrap_or_return!(managed_wallet.build_asset_lock(
wallet_ref.inner(),
key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockFundingAccount::Bip44 {
account_index,
},
&funding_sources,
account_index,
fundings,
fee_per_kb,
false,
Expand Down Expand Up @@ -878,3 +985,63 @@ pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction(
})
}
}

#[cfg(test)]
mod tests {
use super::*;

fn preference(kind: FFIAccountTypePreferenceKind) -> FFIAccountTypePreference {
FFIAccountTypePreference {
kind,
user_identity_id: [7u8; 32],
friend_identity_id: [9u8; 32],
}
}

/// Every kind must map to its own preference. A transposed arm here would
/// silently fund an asset lock from an account the caller did not name —
/// and the build would succeed, so nothing downstream would catch it.
#[test]
fn every_funding_source_kind_maps_to_its_own_preference() {
use FFIAccountTypePreferenceKind as Kind;

assert_eq!(
AccountTypePreference::from(preference(Kind::BIP44)),
AccountTypePreference::BIP44
);
assert_eq!(
AccountTypePreference::from(preference(Kind::BIP32)),
AccountTypePreference::BIP32
);
assert_eq!(
AccountTypePreference::from(preference(Kind::CoinJoin)),
AccountTypePreference::CoinJoin
);
assert_eq!(
AccountTypePreference::from(preference(Kind::AllDashpayReceivingFunds)),
AccountTypePreference::AllDashpayReceivingFunds
);
}

/// The identity IDs ride alongside the tag rather than inside it, so the
/// kinds that read them must read the right ones — and a friendship source
/// must not collapse to the whole-identity one.
#[test]
fn dashpay_sources_carry_the_identity_ids_they_name() {
use FFIAccountTypePreferenceKind as Kind;

assert_eq!(
AccountTypePreference::from(preference(Kind::DashpayFriendshipReceivingFunds)),
AccountTypePreference::DashpayFriendshipReceivingFunds {
user_identity_id: [7u8; 32],
friend_identity_id: [9u8; 32],
}
);
assert_eq!(
AccountTypePreference::from(preference(Kind::DashpayIdentityReceivingFunds)),
AccountTypePreference::DashpayIdentityReceivingFunds {
user_identity_id: [7u8; 32],
}
);
}
}
Loading
Loading