-
Notifications
You must be signed in to change notification settings - Fork 56
feat(kotlin-sdk): union-funding build_signed_payment send API (core primitive + FFI/JNI/Kotlin) #4247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4.2-dev
Are you sure you want to change the base?
feat(kotlin-sdk): union-funding build_signed_payment send API (core primitive + FFI/JNI/Kotlin) #4247
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| //! FFI binding for the union-funding "build a signed payment" primitive. | ||
| //! | ||
| //! Unlike the step-by-step `core_wallet_tx_builder_*` builder (which funds from | ||
| //! a single caller-chosen account), this is a one-shot call that funds a | ||
| //! standard L1 payment from the UNION of every signable funds account | ||
| //! (BIP44 + BIP32 + CoinJoin + DashPay receiving; watch-only DashPay external | ||
| //! accounts are excluded) and returns the **signed serialized transaction | ||
| //! bytes** plus the computed fee and change amount. It does NOT broadcast and | ||
| //! does NOT persist a debit — the caller commits/broadcasts the returned bytes | ||
| //! itself (dashj during the Android transition; a later SDK-broadcast mode | ||
| //! afterwards). See `platform_wallet::wallet::core::send` for the semantics. | ||
|
|
||
| use crate::error::*; | ||
| use crate::handle::{Handle, CORE_WALLET_STORAGE}; | ||
| use crate::runtime::runtime; | ||
| use crate::{check_ptr, unwrap_option_or_return, unwrap_result_or_return}; | ||
| use dashcore::Address as DashAddress; | ||
| use platform_wallet::PlatformWalletError; | ||
| use rs_sdk_ffi::{MnemonicResolverCoreSigner, MnemonicResolverHandle}; | ||
| use std::str::FromStr; | ||
|
|
||
| /// Decode the recipients blob the caller passes to | ||
| /// [`core_wallet_build_signed_payment`]. Layout (big-endian): | ||
| /// | ||
| /// ```text | ||
| /// u32 count | ||
| /// count × ( u32 address_len, address_len bytes (UTF-8), u64 amount_duffs ) | ||
| /// ``` | ||
| /// | ||
| /// Each address is parsed and checked against `network`; a malformed blob or a | ||
| /// wrong-network / unparseable address is a decode error. | ||
| fn decode_payment_outputs( | ||
| blob: &[u8], | ||
| network: dashcore::Network, | ||
| ) -> Result<Vec<(DashAddress, u64)>, PlatformWalletError> { | ||
| let err = |m: String| PlatformWalletError::TransactionBuild(m); | ||
| let mut cursor = 0usize; | ||
| let read_u32 = |buf: &[u8], at: &mut usize| -> Result<u32, PlatformWalletError> { | ||
| let end = *at + 4; | ||
| if end > buf.len() { | ||
| return Err(PlatformWalletError::TransactionBuild( | ||
| "truncated recipients blob (u32)".to_string(), | ||
| )); | ||
| } | ||
| let v = u32::from_be_bytes([buf[*at], buf[*at + 1], buf[*at + 2], buf[*at + 3]]); | ||
| *at = end; | ||
| Ok(v) | ||
| }; | ||
| let read_u64 = |buf: &[u8], at: &mut usize| -> Result<u64, PlatformWalletError> { | ||
| let end = *at + 8; | ||
| if end > buf.len() { | ||
| return Err(PlatformWalletError::TransactionBuild( | ||
| "truncated recipients blob (u64)".to_string(), | ||
| )); | ||
| } | ||
| let mut b = [0u8; 8]; | ||
| b.copy_from_slice(&buf[*at..end]); | ||
| *at = end; | ||
| Ok(u64::from_be_bytes(b)) | ||
| }; | ||
|
|
||
| let count = read_u32(blob, &mut cursor)? as usize; | ||
| let mut outputs = Vec::with_capacity(count); | ||
| for _ in 0..count { | ||
| let addr_len = read_u32(blob, &mut cursor)? as usize; | ||
| let end = cursor + addr_len; | ||
|
Comment on lines
+62
to
+66
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking: Untrusted recipient count can abort the process through an enormous allocation The decoder passes the caller-controlled source: ['codex'] |
||
| if end > blob.len() { | ||
| return Err(err("truncated recipients blob (address)".to_string())); | ||
| } | ||
| let addr_str = std::str::from_utf8(&blob[cursor..end]) | ||
| .map_err(|e| err(format!("recipient address is not valid UTF-8: {e}")))?; | ||
| cursor = end; | ||
| let amount = read_u64(blob, &mut cursor)?; | ||
|
|
||
| let parsed = DashAddress::from_str(addr_str) | ||
| .map_err(|e| err(format!("invalid recipient address {addr_str:?}: {e}")))?; | ||
| let address = parsed | ||
| .require_network(network) | ||
| .map_err(|e| err(format!("recipient address {addr_str:?} network mismatch: {e}")))?; | ||
| outputs.push((address, amount)); | ||
| } | ||
| Ok(outputs) | ||
| } | ||
|
|
||
| /// Build and sign a standard L1 payment from the wallet's signable funds | ||
| /// accounts (union coin selection) and return the signed bytes + fee + change. | ||
| /// | ||
| /// * `handle` — a core-wallet handle (`platform_wallet_get_core`). | ||
| /// * `outputs_blob`/`outputs_blob_len` — the recipients, encoded as documented | ||
| /// on [`decode_payment_outputs`]. | ||
| /// * `fee_per_kb` — fee rate in duffs/kB, or `0` for the default (1000). | ||
| /// * `core_signer_handle` — the caller's `MnemonicResolverHandle`; ownership is | ||
| /// retained by the caller (this function does NOT destroy it). | ||
| /// * `out_tx_bytes`/`out_tx_len` — receive the consensus-serialized signed | ||
| /// transaction. Free with [`core_wallet_free_payment_bytes`]. | ||
| /// * `out_fee` — receives the fee paid, in duffs. | ||
| /// * `out_change` — receives the change returned to the wallet, in duffs (0 if | ||
| /// the build produced no change output). | ||
| /// | ||
| /// # Safety | ||
| /// All pointers must be valid; `outputs_blob` must be readable for | ||
| /// `outputs_blob_len` bytes; the out-pointers must be writable. | ||
| #[no_mangle] | ||
| #[allow(clippy::too_many_arguments)] | ||
| pub unsafe extern "C" fn core_wallet_build_signed_payment( | ||
| handle: Handle, | ||
| outputs_blob: *const u8, | ||
| outputs_blob_len: usize, | ||
| fee_per_kb: u64, | ||
| core_signer_handle: *mut MnemonicResolverHandle, | ||
| out_tx_bytes: *mut *mut u8, | ||
| out_tx_len: *mut usize, | ||
| out_fee: *mut u64, | ||
| out_change: *mut u64, | ||
| ) -> PlatformWalletFFIResult { | ||
| check_ptr!(outputs_blob); | ||
| check_ptr!(core_signer_handle); | ||
| check_ptr!(out_tx_bytes); | ||
| check_ptr!(out_tx_len); | ||
| check_ptr!(out_fee); | ||
| check_ptr!(out_change); | ||
|
|
||
| let blob = std::slice::from_raw_parts(outputs_blob, outputs_blob_len); | ||
| let signer_addr = core_signer_handle as usize; | ||
| let fee = if fee_per_kb == 0 { | ||
| None | ||
| } else { | ||
| Some(fee_per_kb) | ||
| }; | ||
|
|
||
| let option = CORE_WALLET_STORAGE.with_item(handle, |wallet| { | ||
| let network = wallet.network(); | ||
| let outputs = decode_payment_outputs(blob, network)?; | ||
| let wallet_id = wallet.wallet_id(); | ||
| // SAFETY: `signer_addr` came from `core_signer_handle`, which the caller | ||
| // pinned alive for this call; the `MnemonicResolverCoreSigner` lives | ||
| // only on this stack frame and is dropped before returning. | ||
| let signer = MnemonicResolverCoreSigner::new( | ||
| signer_addr as *mut MnemonicResolverHandle, | ||
| wallet_id, | ||
| network, | ||
| ); | ||
| runtime().block_on(wallet.build_signed_payment(outputs, fee, &signer)) | ||
| }); | ||
|
|
||
| let result = unwrap_option_or_return!(option); | ||
| let payment = unwrap_result_or_return!(result); | ||
|
|
||
| let serialized = dashcore::consensus::serialize(&payment.transaction); | ||
| let len = serialized.len(); | ||
| *out_tx_bytes = Box::into_raw(serialized.into_boxed_slice()) as *mut u8; | ||
| *out_tx_len = len; | ||
| *out_fee = payment.fee; | ||
| *out_change = payment.change_amount; | ||
|
|
||
| PlatformWalletFFIResult::ok() | ||
| } | ||
|
|
||
| /// Free the signed-payment bytes returned by [`core_wallet_build_signed_payment`]. | ||
| /// | ||
| /// # Safety | ||
| /// `bytes`/`len` must be the exact pair written to `out_tx_bytes`/`out_tx_len` | ||
| /// by [`core_wallet_build_signed_payment`] (or null / 0). | ||
| #[no_mangle] | ||
| pub unsafe extern "C" fn core_wallet_free_payment_bytes(bytes: *mut u8, len: usize) { | ||
| if !bytes.is_null() && len > 0 { | ||
| let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(bytes, len)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,12 @@ | ||
| pub mod balance; | ||
| pub mod balance_handler; | ||
| mod broadcast; | ||
| mod send; | ||
| mod transaction; | ||
| pub mod wallet; | ||
|
|
||
| pub use balance::WalletBalance; | ||
| pub use balance_handler::BalanceUpdateHandler; | ||
| pub use send::SignedCorePayment; | ||
| pub use transaction::SignedCoreTransaction; | ||
| pub use wallet::CoreWallet; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Untrusted
countdrives an unbounded upfront allocation.countis read directly from the blob header with no relation toblob.len()before being used inVec::with_capacity(count). A malformed/corrupted blob (or a future encoding bug) with a largecounttriggers a large allocation attempt before any row is validated, which can abort the process via OOM instead of returning a cleanTransactionBuilddecode error.🛡️ Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents