Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
09c8aef
feat(swift-sdk): add ZK shielded sync status UI with periodic note an…
QuantumExplorer Mar 20, 2026
d2fdce3
Merge remote-tracking branch 'origin/v3.1-dev' into feat/ios-zk-integ…
QuantumExplorer Mar 20, 2026
5ebd4bc
feat(swift-sdk): add collapsible sync status sections and remove anim…
QuantumExplorer Mar 20, 2026
93205ff
feat(ffi): support local Docker setup with quorum sidecar for regtest
QuantumExplorer Mar 21, 2026
55008db
feat(swift-sdk): add account creation UI, regtest SPV config, and new…
QuantumExplorer Mar 21, 2026
3212d87
fix(swift-sdk): list Platform Payment accounts in wallet account view
QuantumExplorer Mar 21, 2026
822cc24
fix(swift-sdk): use HTTP for local DAPI and list platform payment acc…
QuantumExplorer Mar 21, 2026
2cfe1e8
feat(swift-sdk): add local Docker faucet button and RPC password setting
QuantumExplorer Mar 21, 2026
ac65440
Merge remote-tracking branch 'origin/v3.1-dev' into feat/ios-zk-integ…
QuantumExplorer Mar 26, 2026
033c509
chore: update for key-wallet-manager merge into key-wallet crate
QuantumExplorer Mar 26, 2026
f1ef279
fix(swift-sdk): only show Docker toggle on Local network, auto-disabl…
QuantumExplorer Mar 26, 2026
0bd1341
Merge remote-tracking branch 'origin/v3.1-dev' into feat/ios-zk-integ…
QuantumExplorer Mar 29, 2026
c8d9d86
fix: revert rs-dpp and rs-platform-wallet to v3.1-dev versions
QuantumExplorer Mar 29, 2026
a6671ff
fix(swift-sdk): use Docker peers for SPV when useDockerSetup is enabled
QuantumExplorer Mar 29, 2026
13c6e19
fix(swift-sdk): bump rust-dashcore to a1b2116 and clear default peers…
QuantumExplorer Mar 29, 2026
c38b5a0
fix(swift-sdk): show Platform Payment account addresses in account de…
QuantumExplorer Mar 29, 2026
87320a2
fix(swift-sdk): encode Platform Payment addresses as bech32m in accou…
QuantumExplorer Mar 29, 2026
53adb35
fix(swift-sdk): fix bech32m HRP for platform address detection in Sen…
QuantumExplorer Mar 29, 2026
b367564
fix(swift-sdk): add Core→Platform and Core→Core send flows, fix defau…
QuantumExplorer Mar 29, 2026
e032548
fix(swift-sdk): use stored_height for filter sync progress display
QuantumExplorer Mar 30, 2026
870a0cd
feat(swift-sdk): add quick-fill address buttons in Send view
QuantumExplorer Mar 30, 2026
ab5a56a
chore: bump rust-dashcore to 5db46b4 (adds asset lock transaction bui…
QuantumExplorer Mar 30, 2026
2c27c01
Merge branch 'v3.1-dev' into feat/ios-zk-integration
QuantumExplorer Mar 31, 2026
7fa5afe
feat(swift-sdk): add Swift wrapper for asset lock transaction builder
QuantumExplorer Mar 31, 2026
f2d9088
feat(swift-sdk): add broadcastTransaction to SPVClient
QuantumExplorer Mar 31, 2026
7487f45
feat(swift-sdk): add InstantSend lock store and waitForInstantLock API
QuantumExplorer Mar 31, 2026
808c5f1
feat(swift-sdk): wire up Core→Platform asset lock transfer flow
QuantumExplorer Mar 31, 2026
3d47fac
fix(swift-sdk): convert platform address to P2PKH scriptPubKey for as…
QuantumExplorer Mar 31, 2026
b224aff
fix(swift-sdk): fix dangling pointer in asset lock FFI call
QuantumExplorer Mar 31, 2026
5b1c28f
chore: bump rust-dashcore to 6638745 (fix asset lock coin selection)
QuantumExplorer Mar 31, 2026
c3c9c7b
fix(swift-sdk): replace continuation-based IS lock wait with simple p…
QuantumExplorer Mar 31, 2026
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
20 changes: 10 additions & 10 deletions Cargo.lock

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

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

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

# Optimize heavy crypto crates even in dev/test builds so that
# Halo 2 proof generation and verification run at near-release speed.
Expand Down
54 changes: 40 additions & 14 deletions packages/rs-sdk-ffi/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,21 +331,47 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) -
);

// Create trusted context provider
let trusted_provider = match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new(
network,
None, // Use default quorum lookup endpoints
std::num::NonZeroUsize::new(100).unwrap(), // Cache size
) {
Ok(provider) => {
info!("dash_sdk_create_trusted: trusted context provider created");
Arc::new(provider)
// For local/regtest, use the quorum sidecar at localhost:22444 (dashmate Docker default)
let is_local = matches!(
config.network,
DashSDKNetwork::SDKLocal | DashSDKNetwork::SDKRegtest
);
let trusted_provider = if is_local {
info!("dash_sdk_create_trusted: using local quorum sidecar for regtest");
match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new_with_url(
network,
"http://127.0.0.1:22444".to_string(),
std::num::NonZeroUsize::new(100).unwrap(),
) {
Ok(provider) => {
info!("dash_sdk_create_trusted: local trusted context provider created");
Arc::new(provider)
}
Err(e) => {
error!(error = %e, "dash_sdk_create_trusted: failed to create local context provider");
return DashSDKResult::error(DashSDKError::new(
DashSDKErrorCode::InternalError,
format!("Failed to create local context provider: {}", e),
));
}
}
Err(e) => {
error!(error = %e, "dash_sdk_create_trusted: failed to create trusted context provider");
return DashSDKResult::error(DashSDKError::new(
DashSDKErrorCode::InternalError,
format!("Failed to create trusted context provider: {}", e),
));
} else {
match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new(
network,
None, // Use default quorum lookup endpoints
std::num::NonZeroUsize::new(100).unwrap(), // Cache size
) {
Ok(provider) => {
info!("dash_sdk_create_trusted: trusted context provider created");
Arc::new(provider)
}
Err(e) => {
error!(error = %e, "dash_sdk_create_trusted: failed to create trusted context provider");
return DashSDKResult::error(DashSDKError::new(
DashSDKErrorCode::InternalError,
format!("Failed to create trusted context provider: {}", e),
));
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public enum AccountCategory: Equatable, Hashable, Sendable {
case providerOwnerKeys
case providerOperatorKeys
case providerPlatformKeys
case dashPayReceivingFunds
case dashPayExternalAccount
case platformPayment
}

// MARK: - Account Info
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class SPVClient: @unchecked Sendable {
return dash_spv_ffi_config_mainnet()
case 1:
return dash_spv_ffi_config_testnet()
case 2:
// Regtest (local Docker)
return dash_spv_ffi_config_new(FFINetwork(rawValue: 2))
case 3:
Comment on lines 44 to 50
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Borrowed wallet-manager FFI handle can outlive the owning SPV client

The wallet-manager handle obtained from the FFI is a borrowed pointer whose lifetime is tied to the SPV client. If the SPV client is deallocated while the wallet manager is still in use (e.g., during async transaction operations), the handle becomes a dangling pointer. Consider either copying the handle or ensuring the SPV client's lifetime encompasses all wallet manager usage.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift`:
- [SUGGESTION] lines 1-50: Borrowed wallet-manager FFI handle can outlive the owning SPV client
  The wallet-manager handle obtained from the FFI is a borrowed pointer whose lifetime is tied to the SPV client. If the SPV client is deallocated while the wallet manager is still in use (e.g., during async transaction operations), the handle becomes a dangling pointer. Consider either copying the handle or ensuring the SPV client's lifetime encompasses all wallet manager usage.

Comment on lines 44 to 50
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Core→Platform top-up accepts first InstantSend lock without SPV validation

The top-up flow accepts the first InstantSend lock notification for a transaction without verifying that SPV has actually validated it against the chain. An attacker on the local network could inject a fake IS notification. The code should verify the transaction's confirmation status via SPV before crediting the platform account.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift`:
- [SUGGESTION] lines 1-50: Core→Platform top-up accepts first InstantSend lock without SPV validation
  The top-up flow accepts the first InstantSend lock notification for a transaction without verifying that SPV has actually validated it against the chain. An attacker on the local network could inject a fake IS notification. The code should verify the transaction's confirmation status via SPV before crediting the platform account.

// Map devnet to custom FFINetwork value 3
return dash_spv_ffi_config_new(FFINetwork(rawValue: 3))
Expand All @@ -58,13 +61,16 @@ class SPVClient: @unchecked Sendable {

// If requested, prefer local core peers (defaults to 127.0.0.1 with network default port)
let useLocalCore = UserDefaults.standard.bool(forKey: "useLocalhostCore")
|| UserDefaults.standard.bool(forKey: "useDockerSetup")
// Only restrict to configured peers when using local core, if not, allow DNS discovery
let restrictToConfiguredPeers = useLocalCore
if useLocalCore {
let peers = SPVClient.readLocalCorePeers()
if swiftLoggingEnabled {
print("[SPV][Config] Use Local Core enabled; peers=\(peers.joined(separator: ", "))")
}
// Clear default peers before adding custom Docker peers
dash_spv_ffi_config_clear_peers(configPtr)
// Add peers via FFI (supports "ip:port" or bare IP for network-default port)
for addr in peers {
addr.withCString { cstr in
Expand Down Expand Up @@ -135,9 +141,9 @@ class SPVClient: @unchecked Sendable {
}

private static func readLocalCorePeers() -> [String] {
// If no override is set, default to 127.0.0.1 and let FFI pick port by network
// If no override is set, default to dashmate Docker Core P2P port
let raw = UserDefaults.standard.string(forKey: "corePeerAddresses")?.trimmingCharacters(in: .whitespacesAndNewlines)
let list = (raw?.isEmpty == false ? raw! : "127.0.0.1")
let list = (raw?.isEmpty == false ? raw! : "127.0.0.1:20001")
return list
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
Expand Down Expand Up @@ -190,6 +196,25 @@ class SPVClient: @unchecked Sendable {
config = nil
}

// MARK: - Broadcast Transactions

func broadcastTransaction(_ transactionData: Data) throws {
try transactionData.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in
guard let txBytes = ptr.bindMemory(to: UInt8.self).baseAddress else {
throw SPVError.transactionBroadcastFailed("Invalid transaction data pointer")
}
let result = dash_spv_ffi_client_broadcast_transaction(
client,
txBytes,
UInt(transactionData.count)
)

if result != 0 {
throw SPVError.transactionBroadcastFailed(SPVClient.getLastDashFFIError())
}
}
}

// MARK: - Synchronization

func startSync() async throws {
Expand Down Expand Up @@ -235,6 +260,7 @@ public enum SPVError: LocalizedError {
case alreadySyncing
case syncFailed(String)
case storageOperationFailed(String)
case transactionBroadcastFailed(String)

public var errorDescription: String? {
switch self {
Expand All @@ -254,6 +280,8 @@ public enum SPVError: LocalizedError {
return "Sync failed: \(reason)"
case let .storageOperationFailed(reason):
return reason
case let .transactionBroadcastFailed(reason):
return "Transaction broadcast failed: \(reason)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public class WalletService: ObservableObject {
private var spvClient: SPVClient
public private(set) var walletManager: CoreWalletManager

// InstantSend lock storage for asset lock flow
private let instantLockStore = InstantLockStore()

public init(modelContainer: ModelContainer, network: AppNetwork) {
self.modelContainer = modelContainer
self.network = network
Expand Down Expand Up @@ -236,6 +239,19 @@ public class WalletService: ObservableObject {
self.initializeNewSPVClient()
}

// MARK: - Transaction Broadcasting

/// Broadcast a raw transaction on the Core P2P network.
public func broadcastTransaction(_ transactionData: Data) throws {
try spvClient.broadcastTransaction(transactionData)
}

/// Wait for an InstantSend lock for a specific transaction.
/// Returns the serialized IS lock bytes when received, or throws on timeout.
public func waitForInstantLock(txid: Data, timeout: TimeInterval = 30) async throws -> Data {
try await instantLockStore.waitForLock(txid: txid, timeout: timeout)
}

public func clearSpvStorage() {
if syncProgress.state.isRunning() {
print("[SPV][Clear] Sync task is running, cannot clear storage")
Expand Down Expand Up @@ -314,7 +330,9 @@ public class WalletService: ObservableObject {
func onBlocksProcessed(_ height: UInt32, _ hash: Data, _ newAddressCount: UInt32) {}
func onMasternodeStateUpdated(_ height: UInt32) {}
func onChainLockReceived(_ height: UInt32, _ hash: Data, _ signature: Data, _ validated: Bool) {}
func onInstantLockReceived(_ txid: Data, _ instantLockData: Data, _ validated: Bool) {}
func onInstantLockReceived(_ txid: Data, _ instantLockData: Data, _ validated: Bool) {
walletService.instantLockStore.store(txid: txid, lockData: instantLockData)
}
func onSyncManagerError(_ manager: SPVSyncManager, _ errorMsg: String) {
SDKLogger.error("Sync manager \(manager) error: \(errorMsg)")

Expand Down Expand Up @@ -393,3 +411,45 @@ extension Data {
return map { String(format: "%02hhx", $0) }.joined()
}
}

// MARK: - InstantSend Lock Store

/// Thread-safe store for InstantSend lock data, keyed by txid.
/// Supports async waiting for a specific txid's IS lock to arrive.
internal final class InstantLockStore: @unchecked Sendable {
private var locks: [Data: Data] = [:]
private var continuations: [Data: CheckedContinuation<Data, Error>] = [:]
private let queue = DispatchQueue(label: "com.dash.instantlock-store")

/// Store an IS lock. Resumes waiter if one exists for this txid.
func store(txid: Data, lockData: Data) {
var cont: CheckedContinuation<Data, Error>?
queue.sync {
locks[txid] = lockData
cont = continuations.removeValue(forKey: txid)
}
cont?.resume(returning: lockData)
}

/// Wait for an IS lock for a specific txid.
/// Returns immediately if already cached, otherwise polls until received or timeout.
func waitForLock(txid: Data, timeout: TimeInterval = 30) async throws -> Data {
// Check if already available
if let existing = queue.sync(execute: { locks[txid] }) {
return existing
}

// Poll-based approach — simpler and avoids continuation resume races
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
try await Task.sleep(nanoseconds: 250_000_000) // 250ms
if let existing = queue.sync(execute: { locks[txid] }) {
return existing
}
}

throw SPVError.transactionBroadcastFailed(
"InstantSend lock timeout after \(Int(timeout))s for txid \(txid.hexString)"
)
}
}
Loading
Loading