From 603510f8d86a8860ae7ef4cba5c75344cc8b8bfd Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 21 Jul 2026 01:10:20 +0800 Subject: [PATCH 1/4] fix(ffi): enforce boundary lifetime invariants --- .../rs-platform-wallet/src/manager/mod.rs | 17 ++- .../rs-platform-wallet/src/spv/runtime.rs | 69 ++++++++-- .../identity/network/payment_handler.rs | 118 +++++++++++++++++- .../rs-sdk-ffi/src/document/queries/info.rs | 96 +++++++++++++- .../PlatformWalletManager.swift | 5 + 5 files changed, 288 insertions(+), 17 deletions(-) diff --git a/packages/rs-platform-wallet/src/manager/mod.rs b/packages/rs-platform-wallet/src/manager/mod.rs index 7962d6551f1..3a0016a4b02 100644 --- a/packages/rs-platform-wallet/src/manager/mod.rs +++ b/packages/rs-platform-wallet/src/manager/mod.rs @@ -64,6 +64,9 @@ pub struct PlatformWalletManager { /// auto-started — call `start` after wallets are registered. See /// [`DashPaySyncManager`]. pub(super) dashpay_sync_manager: Arc, + /// Tracks asynchronous payment hooks so manager shutdown can close + /// admission and drain every task before host callback contexts are freed. + pub(super) dashpay_payment_handler: Arc, /// Periodic shielded (Orchard) note sync coordinator (spends are /// detected during the note scan, no separate nullifier pass). /// Iterates every wallet that has been bound via @@ -147,7 +150,7 @@ impl PlatformWalletManager

{ app_handler, lock_handler, balance_handler, - dashpay_payment_handler, + Arc::clone(&dashpay_payment_handler) as Arc, ])); let spv = Arc::new(SpvRuntime::new( @@ -183,6 +186,7 @@ impl PlatformWalletManager

{ platform_address_sync_manager: platform_address_sync, identity_sync_manager: identity_sync, dashpay_sync_manager: dashpay_sync, + dashpay_payment_handler, #[cfg(feature = "shielded")] shielded_sync_manager: shielded_sync, #[cfg(feature = "shielded")] @@ -353,7 +357,7 @@ impl PlatformWalletManager

{ /// Stop all background tasks and wait for them to exit. /// - /// **Quiesces** the periodic coordinators + /// Stops SPV and **quiesces** the periodic coordinators /// (`PlatformAddressSyncManager`, `IdentitySyncManager`, /// `DashPaySyncManager`, `ShieldedSyncManager`) — cancelling each /// loop *and draining any in-flight pass to completion*, including @@ -363,7 +367,9 @@ impl PlatformWalletManager

{ /// shutdown is required (e.g. on app termination); a dirty drop /// simply leaks the tasks until the runtime exits. /// - /// Ordering matters: cancel-only `stop()` would let a pass already + /// Ordering matters: SPV is stopped and joined first so it cannot dispatch + /// more wallet events. Payment-task admission is then closed and all + /// admitted work is joined. A cancel-only `stop()` would let a pass already /// inside `sync_now` keep running and call `persister.store(...)` / /// fire a host completion callback after the FFI's `destroy` /// returned and the host freed the persister / event-handler @@ -372,6 +378,11 @@ impl PlatformWalletManager

{ /// and only THEN cancel + join the event adapter, which is the sink /// those stores feed into. pub async fn shutdown(&self) { + if let Err(error) = self.spv_manager.stop().await { + tracing::warn!(?error, "SPV shutdown failed"); + } + + self.dashpay_payment_handler.quiesce().await; self.platform_address_sync_manager.quiesce().await; self.identity_sync_manager.quiesce().await; self.dashpay_sync_manager.quiesce().await; diff --git a/packages/rs-platform-wallet/src/spv/runtime.rs b/packages/rs-platform-wallet/src/spv/runtime.rs index 13d5d9cf5a2..abf9de0cd46 100644 --- a/packages/rs-platform-wallet/src/spv/runtime.rs +++ b/packages/rs-platform-wallet/src/spv/runtime.rs @@ -1,6 +1,7 @@ //! SPV client runtime — manages the DashSpvClient lifecycle. use std::sync::{Arc, Mutex}; +use std::time::Duration; use tokio::sync::RwLock; use tokio::task::JoinHandle; @@ -24,6 +25,28 @@ use crate::wallet::platform_wallet::PlatformWalletInfo; type SpvClient = DashSpvClient, PeerNetworkManager, DiskStorageManager>; +const SPV_STOP_TIMEOUT: Duration = Duration::from_secs(15); + +/// Join a stopped SPV runner, escalating to cancellation after `timeout` but +/// never returning until Tokio confirms that the task has terminated. +async fn join_spv_task(mut handle: JoinHandle<()>, timeout: Duration) { + match tokio::time::timeout(timeout, &mut handle).await { + Ok(Ok(())) => {} + Ok(Err(error)) => { + tracing::warn!(?error, "SPV background run loop join error"); + } + Err(_) => { + tracing::warn!("SPV stop: background run loop did not unwind in time; aborting it"); + handle.abort(); + if let Err(error) = handle.await { + if !error.is_cancelled() { + tracing::warn!(?error, "SPV background run loop abort join error"); + } + } + } + } +} + /// SPV client runtime — owns the `DashSpvClient` and drives sync. /// /// Events are dispatched through [`PlatformEventManager`] to all registered @@ -217,17 +240,7 @@ impl SpvRuntime { let handle = self.task.lock().expect("spv task mutex poisoned").take(); if let Some(handle) = handle { - let abort = handle.abort_handle(); - if tokio::time::timeout(std::time::Duration::from_secs(15), handle) - .await - .is_err() - { - tracing::warn!( - "SPV stop: background run loop did not unwind within 15s; aborting it" - ); - - abort.abort(); - } + join_spv_task(handle, SPV_STOP_TIMEOUT).await; } stop_result @@ -415,6 +428,40 @@ impl SpvRuntime { } } +#[cfg(test)] +mod shutdown_tests { + use super::*; + use std::sync::atomic::{AtomicBool, Ordering}; + + struct DropFlag(Arc); + + impl Drop for DropFlag { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + + #[tokio::test(start_paused = true)] + async fn timed_out_spv_task_is_aborted_and_joined_before_return() { + let dropped = Arc::new(AtomicBool::new(false)); + let dropped_in_task = Arc::clone(&dropped); + let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let handle = tokio::spawn(async move { + let _flag = DropFlag(dropped_in_task); + let _ = started_tx.send(()); + std::future::pending::<()>().await; + }); + started_rx.await.expect("SPV task should start"); + + join_spv_task(handle, SPV_STOP_TIMEOUT).await; + + assert!( + dropped.load(Ordering::SeqCst), + "abort must be joined so task-owned callback state is dropped" + ); + } +} + impl std::fmt::Debug for SpvRuntime { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SpvRuntime") diff --git a/packages/rs-platform-wallet/src/wallet/identity/network/payment_handler.rs b/packages/rs-platform-wallet/src/wallet/identity/network/payment_handler.rs index 0808e8c0116..db9423efa35 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/network/payment_handler.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/network/payment_handler.rs @@ -24,6 +24,7 @@ //! to a core transaction row. use std::sync::Arc; +use std::{future::Future, sync::Mutex}; use dash_spv::EventHandler; use key_wallet::managed_account::transaction_record::TransactionRecord; @@ -44,6 +45,70 @@ use crate::wallet::platform_wallet::PlatformWalletInfo; pub(crate) struct DashPayPaymentHandler { wallet_manager: Arc>>, persister: Arc, + tasks: PaymentTaskTracker, +} + +struct PaymentTaskState { + accepting: bool, + handles: Vec>, +} + +/// Admission gate and join barrier for asynchronous payment hooks. +/// +/// Event dispatch is synchronous, so the gate uses a short standard mutex: +/// checking admission and recording the resulting task happen atomically with +/// respect to shutdown. No mutex guard is held while a task is awaited. +struct PaymentTaskTracker { + state: Mutex, +} + +impl PaymentTaskTracker { + fn new() -> Self { + Self { + state: Mutex::new(PaymentTaskState { + accepting: true, + handles: Vec::new(), + }), + } + } + + fn spawn(&self, future: F) -> bool + where + F: Future + Send + 'static, + { + let mut state = self.state.lock().expect("payment task mutex poisoned"); + if !state.accepting { + return false; + } + + // Completed tasks cannot fire another callback and need not be kept + // until manager destruction. This bounds the tracker during long syncs. + state.handles.retain(|handle| !handle.is_finished()); + state.handles.push(tokio::spawn(future)); + true + } + + async fn quiesce(&self) { + let handles = { + let mut state = self.state.lock().expect("payment task mutex poisoned"); + state.accepting = false; + std::mem::take(&mut state.handles) + }; + + for handle in handles { + if let Err(error) = handle.await { + tracing::warn!(?error, "DashPay payment task join error"); + } + } + } + + #[cfg(test)] + fn is_accepting(&self) -> bool { + self.state + .lock() + .expect("payment task mutex poisoned") + .accepting + } } impl DashPayPaymentHandler { @@ -54,8 +119,15 @@ impl DashPayPaymentHandler { Self { wallet_manager, persister, + tasks: PaymentTaskTracker::new(), } } + + /// Stop admitting callback-bearing work and join every task admitted + /// before the gate closed. Idempotent. + pub(crate) async fn quiesce(&self) { + self.tasks.quiesce().await; + } } impl EventHandler for DashPayPaymentHandler { @@ -69,7 +141,7 @@ impl EventHandler for DashPayPaymentHandler { let wallet_manager = Arc::clone(&self.wallet_manager); let persister = Arc::clone(&self.persister); let event = event.clone(); - tokio::spawn(async move { + self.tasks.spawn(async move { let wallet_id = event.wallet_id(); let wallet_persister = crate::wallet::persister::WalletPersister::new(wallet_id, persister); @@ -185,6 +257,7 @@ mod tests { use key_wallet::managed_account::transaction_record::TransactionDirection; use key_wallet::transaction_checking::{TransactionContext, TransactionType}; use key_wallet::WalletCoreBalance; + use std::sync::atomic::{AtomicBool, Ordering}; /// A `TransactionRecord` whose txid is uniquely seeded by `seed` (via a /// distinct input outpoint). Context is irrelevant to the routing under @@ -323,4 +396,47 @@ mod tests { assert!(dashpay_payment_records(&event).is_empty()); assert!(!drives_payment_hooks(&event)); } + + #[tokio::test] + async fn payment_task_quiesce_closes_admission_and_joins_in_flight_work() { + let tasks = Arc::new(PaymentTaskTracker::new()); + let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let (release_tx, release_rx) = tokio::sync::oneshot::channel(); + let completed = Arc::new(AtomicBool::new(false)); + let completed_by_task = Arc::clone(&completed); + + assert!(tasks.spawn(async move { + let _ = started_tx.send(()); + let _ = release_rx.await; + completed_by_task.store(true, Ordering::SeqCst); + })); + started_rx.await.expect("tracked task should start"); + + let tasks_for_shutdown = Arc::clone(&tasks); + let shutdown = tokio::spawn(async move { + tasks_for_shutdown.quiesce().await; + }); + + while tasks.is_accepting() { + tokio::task::yield_now().await; + } + assert!(!shutdown.is_finished(), "quiesce must await admitted work"); + + let ran_after_close = Arc::new(AtomicBool::new(false)); + let ran_after_close_in_task = Arc::clone(&ran_after_close); + assert!(!tasks.spawn(async move { + ran_after_close_in_task.store(true, Ordering::SeqCst); + })); + + release_tx + .send(()) + .expect("tracked task should still exist"); + shutdown.await.expect("quiesce task should join"); + assert!(completed.load(Ordering::SeqCst)); + assert!(!ran_after_close.load(Ordering::SeqCst)); + + // Repeated shutdown is safe and keeps admission closed. + tasks.quiesce().await; + assert!(!tasks.spawn(async {})); + } } diff --git a/packages/rs-sdk-ffi/src/document/queries/info.rs b/packages/rs-sdk-ffi/src/document/queries/info.rs index 3e5a60ac560..ae5ef1bd38b 100644 --- a/packages/rs-sdk-ffi/src/document/queries/info.rs +++ b/packages/rs-sdk-ffi/src/document/queries/info.rs @@ -359,8 +359,15 @@ pub unsafe extern "C" fn dash_sdk_document_get_info( }); } + // Keep the allocation length and the exported count under the same + // authority. Some Platform values (notably text containing an embedded + // NUL) cannot be represented by this C-string ABI and are skipped above; + // using the original property count would let consumers walk and free + // past the filtered allocation. + let data_fields_count = data_fields.len(); + // Convert vector to raw pointer - let data_fields_ptr = if data_fields.is_empty() { + let data_fields_ptr = if data_fields_count == 0 { std::ptr::null_mut() } else { let mut fields = data_fields.into_boxed_slice(); @@ -377,9 +384,94 @@ pub unsafe extern "C" fn dash_sdk_document_get_info( revision: document.revision().unwrap_or(0), created_at: document.created_at().map(|t| t as i64).unwrap_or(0), updated_at: document.updated_at().map(|t| t as i64).unwrap_or(0), - data_fields_count: properties.len(), + data_fields_count, data_fields: data_fields_ptr, }; Box::into_raw(Box::new(info)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::dash_sdk_document_info_free; + use dash_sdk::dpp::document::DocumentV0; + use dash_sdk::dpp::platform_value::Identifier; + use std::collections::BTreeMap; + use std::ffi::CStr; + + fn document_with_properties(properties: BTreeMap) -> Document { + Document::V0(DocumentV0 { + id: Identifier::new([1; 32]), + owner_id: Identifier::new([2; 32]), + properties, + ..Default::default() + }) + } + + unsafe fn document_info(document: &Document) -> *mut DashSDKDocumentInfo { + dash_sdk_document_get_info(document as *const Document as *const DocumentHandle) + } + + #[test] + fn embedded_nul_value_does_not_inflate_exported_field_count() { + let document = document_with_properties(BTreeMap::from([ + ("kept".to_string(), Value::Text("ordinary".to_string())), + ( + "skipped".to_string(), + Value::Text("left\0right".to_string()), + ), + ])); + + unsafe { + let info = document_info(&document); + assert!(!info.is_null()); + assert_eq!((*info).data_fields_count, 1); + assert!(!(*info).data_fields.is_null()); + + let field = &*(*info).data_fields; + assert_eq!(CStr::from_ptr(field.name).to_bytes(), b"kept"); + assert_eq!(CStr::from_ptr(field.value).to_bytes(), b"ordinary"); + + // Exercises the production destructor with the exported count. + dash_sdk_document_info_free(info); + } + } + + #[test] + fn all_unrepresentable_fields_export_a_null_empty_slice() { + let document = document_with_properties(BTreeMap::from([( + "skipped".to_string(), + Value::Text("left\0right".to_string()), + )])); + + unsafe { + let info = document_info(&document); + assert!(!info.is_null()); + assert_eq!((*info).data_fields_count, 0); + assert!((*info).data_fields.is_null()); + dash_sdk_document_info_free(info); + } + } + + #[test] + fn multiple_filtered_fields_preserve_exact_allocation_length() { + let document = document_with_properties(BTreeMap::from([ + ("a-skipped".to_string(), Value::Text("a\0b".to_string())), + ("m-kept".to_string(), Value::I64(42)), + ("z-skipped".to_string(), Value::Text("c\0d".to_string())), + ])); + + unsafe { + let info = document_info(&document); + assert!(!info.is_null()); + assert_eq!((*info).data_fields_count, 1); + assert!(!(*info).data_fields.is_null()); + assert_eq!( + CStr::from_ptr((*(*info).data_fields).name).to_bytes(), + b"m-kept" + ); + dash_sdk_document_info_free(info); + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift index f2255597c2a..5957ea30570 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift @@ -229,6 +229,11 @@ public class PlatformWalletManager: ObservableObject { deinit { progressPollTask?.cancel() if handle != NULL_HANDLE { + // Stop the network event source before releasing the manager's + // unretained callback contexts. Rust's destroy path provides the + // authoritative join barrier; this explicit stop is defense in + // depth for the Swift wrapper's teardown order. + platform_wallet_manager_spv_stop(handle).discard() platform_wallet_manager_platform_address_sync_stop(handle).discard() platform_wallet_manager_shielded_sync_stop(handle).discard() platform_wallet_manager_dashpay_sync_stop(handle).discard() From 379545104673bc6ea44efbc256a04e33a8e188c2 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 21 Jul 2026 14:44:12 +0800 Subject: [PATCH 2/4] fix(ci): bound Swift SDK build disk usage --- .github/workflows/swift-sdk-build.yml | 4 ++ packages/swift-sdk/build_ios.sh | 55 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/.github/workflows/swift-sdk-build.yml b/.github/workflows/swift-sdk-build.yml index a6a2c06445b..55b73e71084 100644 --- a/.github/workflows/swift-sdk-build.yml +++ b/.github/workflows/swift-sdk-build.yml @@ -87,5 +87,9 @@ jobs: rm -rf packages/swift-sdk/SwiftExampleApp/.build || true - name: Build and test Swift SDK + env: + # The self-hosted runner persists Cargo's target cache between jobs. + # Keep only one Apple architecture's intermediates at a time. + PRUNE_CARGO_TARGETS: "1" run: | bash packages/swift-sdk/run_tests.sh diff --git a/packages/swift-sdk/build_ios.sh b/packages/swift-sdk/build_ios.sh index 0cd72299a6f..59e3a77760b 100755 --- a/packages/swift-sdk/build_ios.sh +++ b/packages/swift-sdk/build_ios.sh @@ -24,6 +24,8 @@ TARGET_DIR="$ROOT_DIR/target" PACKAGE="rs-unified-sdk-ffi" XCFRAMEWORK="$SCRIPT_DIR/DashSDKFFI.xcframework" PROFILE="dev" +PRUNE_CARGO_TARGETS="${PRUNE_CARGO_TARGETS:-0}" +STAGING_DIR="" # Crates whose cbindgen-generated headers ship in the unified framework. # Order matters: earlier headers define types referenced by later ones. @@ -46,6 +48,31 @@ CLEAN=false log_info() { echo -e "${GREEN}$1${NC}"; } log_error() { echo -e "${RED}$1${NC}"; } +cleanup_staging_dir() { + if [ -n "$STAGING_DIR" ]; then + rm -rf "$STAGING_DIR" + fi +} + +stage_target_artifacts() { + local target="$1" + local library="$2" + local headers="$3" + local target_staging_dir="$STAGING_DIR/$target" + + mkdir -p "$target_staging_dir" + cp "$library" "$target_staging_dir/" + cp -R "$headers" "$target_staging_dir/include" + + STAGED_LIB="$target_staging_dir/$(basename "$library")" + STAGED_HEADERS="$target_staging_dir/include" + + # The final static library and generated headers are all xcodebuild needs. + # Release the much larger per-architecture dependency tree before building + # the next target so persistent CI runners cannot exhaust their disk. + rm -rf "$TARGET_DIR/$target" +} + # ------------------------------- # Help # ------------------------------- @@ -125,6 +152,19 @@ OUTPUT_DIR="$PROFILE" log_info "Package: $PACKAGE" log_info "Profile: $PROFILE" +if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then + STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/dash-sdk-ffi.XXXXXX")" + trap cleanup_staging_dir EXIT + + # Persistent self-hosted runners may contain incomplete or obsolete builds + # from an earlier job. Start the bounded build with only Cargo's shared host + # cache, then prune each Apple target after staging its final artifacts. + rm -rf \ + "$TARGET_DIR/aarch64-apple-ios" \ + "$TARGET_DIR/aarch64-apple-ios-sim" \ + "$TARGET_DIR/aarch64-apple-darwin" +fi + # ------------------------------- # Build commands # ------------------------------- @@ -195,6 +235,11 @@ if $BUILD_IOS; then IOS_LIB="$TARGET_DIR/$IOS_TARGET/$OUTPUT_DIR/librs_unified_sdk_ffi.a" IOS_HEADERS="$TARGET_DIR/$IOS_TARGET/$OUTPUT_DIR/include" inject_modulemap "$IOS_HEADERS" + if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then + stage_target_artifacts "$IOS_TARGET" "$IOS_LIB" "$IOS_HEADERS" + IOS_LIB="$STAGED_LIB" + IOS_HEADERS="$STAGED_HEADERS" + fi fi # iOS simulator @@ -205,6 +250,11 @@ if $BUILD_SIM; then SIM_LIB="$TARGET_DIR/$SIM_TARGET/$OUTPUT_DIR/librs_unified_sdk_ffi.a" SIM_HEADERS="$TARGET_DIR/$SIM_TARGET/$OUTPUT_DIR/include" inject_modulemap "$SIM_HEADERS" + if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then + stage_target_artifacts "$SIM_TARGET" "$SIM_LIB" "$SIM_HEADERS" + SIM_LIB="$STAGED_LIB" + SIM_HEADERS="$STAGED_HEADERS" + fi fi # macOS @@ -215,6 +265,11 @@ if $BUILD_MAC; then MAC_LIB="$TARGET_DIR/$MAC_TARGET/$OUTPUT_DIR/librs_unified_sdk_ffi.a" MAC_HEADERS="$TARGET_DIR/$MAC_TARGET/$OUTPUT_DIR/include" inject_modulemap "$MAC_HEADERS" + if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then + stage_target_artifacts "$MAC_TARGET" "$MAC_LIB" "$MAC_HEADERS" + MAC_LIB="$STAGED_LIB" + MAC_HEADERS="$STAGED_HEADERS" + fi fi # ------------------------------- From afc1776c3090df1a3fe1e41257bba3cc02a00cce Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 03:43:39 +0800 Subject: [PATCH 3/4] fix(ci): guard Swift SDK cleanup paths --- packages/swift-sdk/build_ios.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/swift-sdk/build_ios.sh b/packages/swift-sdk/build_ios.sh index 59e3a77760b..b858f846fb2 100755 --- a/packages/swift-sdk/build_ios.sh +++ b/packages/swift-sdk/build_ios.sh @@ -70,7 +70,7 @@ stage_target_artifacts() { # The final static library and generated headers are all xcodebuild needs. # Release the much larger per-architecture dependency tree before building # the next target so persistent CI runners cannot exhaust their disk. - rm -rf "$TARGET_DIR/$target" + rm -rf "${TARGET_DIR:?}/${target:?}" } # ------------------------------- @@ -132,7 +132,7 @@ done if $CLEAN; then log_info "Cleaning all build artifacts..." - rm -rf "$TARGET_DIR" + rm -rf "${TARGET_DIR:?}" rm -rf "$XCFRAMEWORK" fi @@ -160,9 +160,9 @@ if [ "$PRUNE_CARGO_TARGETS" = "1" ]; then # from an earlier job. Start the bounded build with only Cargo's shared host # cache, then prune each Apple target after staging its final artifacts. rm -rf \ - "$TARGET_DIR/aarch64-apple-ios" \ - "$TARGET_DIR/aarch64-apple-ios-sim" \ - "$TARGET_DIR/aarch64-apple-darwin" + "${TARGET_DIR:?}/aarch64-apple-ios" \ + "${TARGET_DIR:?}/aarch64-apple-ios-sim" \ + "${TARGET_DIR:?}/aarch64-apple-darwin" fi # ------------------------------- From 53a091e3269e2e7de179112631e7b12b9ac18bcf Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Jul 2026 03:55:38 +0800 Subject: [PATCH 4/4] style(jni): format transaction decoder --- packages/rs-unified-sdk-jni/src/tx_decode.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/rs-unified-sdk-jni/src/tx_decode.rs b/packages/rs-unified-sdk-jni/src/tx_decode.rs index 3dc8cb840ad..02aeafc3f49 100644 --- a/packages/rs-unified-sdk-jni/src/tx_decode.rs +++ b/packages/rs-unified-sdk-jni/src/tx_decode.rs @@ -130,7 +130,13 @@ fn decode_to_blob(tx_bytes: &[u8], network: FFINetwork) -> Result, (i32, }; let mut out: *mut DecodedTransactionFFI = std::ptr::null_mut(); let ok = unsafe { - transaction_decode(tx_bytes.as_ptr(), tx_bytes.len(), network, &mut out, &mut error) + transaction_decode( + tx_bytes.as_ptr(), + tx_bytes.len(), + network, + &mut out, + &mut error, + ) }; if !ok || out.is_null() { let message = if error.message.is_null() { @@ -281,7 +287,10 @@ mod tests { let bytes = serialize(&tx); let blob = decode_to_blob(&bytes, FFINetwork::Testnet).expect("decode ok"); - let mut r = Reader { blob: &blob, pos: 0 }; + let mut r = Reader { + blob: &blob, + pos: 0, + }; assert_eq!(r.take(32), tx.txid().to_byte_array()); assert_eq!(r.u32(), 1, "one input"); @@ -321,7 +330,10 @@ mod tests { fn network_changes_rendered_addresses() { let (tx, addr) = p2pkh_spend_tx(Network::Testnet); let blob = decode_to_blob(&serialize(&tx), FFINetwork::Mainnet).expect("decode ok"); - let mut r = Reader { blob: &blob, pos: 0 }; + let mut r = Reader { + blob: &blob, + pos: 0, + }; r.take(32); r.u32(); r.take(36);