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
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class SyncingActivityMonitor: NSObject, NetworkReachabilityHandling {
@objc
public var progress: Double = 0 {
didSet {
observers.forEach { $0.syncingActivityMonitorProgressDidChange(progress) }
observerSnapshot().forEach { $0.syncingActivityMonitorProgressDidChange(progress) }
}
}

Expand All @@ -157,7 +157,7 @@ class SyncingActivityMonitor: NSObject, NetworkReachabilityHandling {
kSyncStateChangedNewStateKey: state,
])

observers.forEach { $0.syncingActivityMonitorStateDidChange(previousState: oldValue, state: state) }
observerSnapshot().forEach { $0.syncingActivityMonitorStateDidChange(previousState: oldValue, state: state) }
}
}

Expand All @@ -178,7 +178,8 @@ class SyncingActivityMonitor: NSObject, NetworkReachabilityHandling {
private var lastPeakDate: Date?
private var cancellables = Set<AnyCancellable>()

private lazy var observers: [SyncingActivityMonitorObserver] = []
private var observers: [SyncingActivityMonitorObserver] = []
private let observersLock = NSLock()

override init() {
super.init()
Expand All @@ -196,19 +197,32 @@ class SyncingActivityMonitor: NSObject, NetworkReachabilityHandling {

@objc(addObserver:)
public func add(observer: SyncingActivityMonitorObserver) {
observersLock.lock()
defer { observersLock.unlock() }
observers.append(observer)
}

@objc(removeObserver:)
public func remove(observer: SyncingActivityMonitorObserver) {
observersLock.lock()
defer { observersLock.unlock() }
if let idx = observers.firstIndex(where: { $0 === observer }) {
observers.remove(at: idx)
}
}

private func observerSnapshot() -> [SyncingActivityMonitorObserver] {
observersLock.lock()
defer { observersLock.unlock() }
return observers
}

deinit {
NotificationCenter.default.removeObserver(self)
NotificationCenter.default.removeObserver(reachabilityObserver!)
if let observer = reachabilityObserver {
NotificationCenter.default.removeObserver(observer)
reachabilityObserver = nil
}
}

/// ObjC-visible accessor for `.syncStateChangedNotification` (same
Expand Down
17 changes: 8 additions & 9 deletions DashWallet/Sources/Categories/NumberFormatter+DashWallet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,25 @@ extension NumberFormatter {
return formattedString
}

var currencySymbolRange: Range<String.Index>! = formattedString.range(of: currencySymbol)
var currencySymbolRange = formattedString.range(of: currencySymbol)

if currencySymbolRange == nil && currencySymbol.count != numberFormatter.currencySymbol.count {
assertionFailure("Invalid formatted string")
return nil
} else if currencySymbolRange == nil {
currencySymbolRange = formattedString.range(of: numberFormatter.currencySymbol)
}

guard let currencySymbolRange else {
fatalError("Invalid formatted string")
return nil
}

let isCurrencySymbolAtTheBeginning = currencySymbolRange.lowerBound == formattedString.startIndex
var currencySymbolNumberSeparator: String

if isCurrencySymbolAtTheBeginning {
currencySymbolNumberSeparator =
String(formattedString[
currencySymbolRange.upperBound..<formattedString
.index(after: currencySymbolRange.upperBound)
])
let separatorStart = currencySymbolRange.upperBound
guard separatorStart < formattedString.endIndex else { return nil }
currencySymbolNumberSeparator = String(formattedString[separatorStart..<formattedString.index(after: separatorStart)])
} else {
currencySymbolNumberSeparator =
String(formattedString[
Expand All @@ -85,8 +83,9 @@ extension NumberFormatter {
var formattedSeparatorIndex: String.Index! = formattedStringWithoutCurrency.range(of: decimalSeparator)?.lowerBound

if formattedSeparatorIndex == nil {
formattedSeparatorIndex = formattedStringWithoutCurrency.endIndex
formattedStringWithoutCurrency = formattedStringWithoutCurrency + decimalSeparator
formattedSeparatorIndex = formattedStringWithoutCurrency.index(formattedStringWithoutCurrency.endIndex,
offsetBy: -decimalSeparator.count)
}

let formattedFractionPartRange = formattedSeparatorIndex..<formattedStringWithoutCurrency.endIndex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ class DatabaseConnection: NSObject {
var migrationManager: SQLiteMigrationManager!

override init() {
print("SQLite: ", DatabaseConnection.storeURL().absoluteString)
let databaseURL = DatabaseConnection.storeURL()
print("SQLite: ", databaseURL.path)
do {
db = try Connection(DatabaseConnection.storeURL().absoluteString)
db = try Connection(databaseURL.path)
migrationManager = SQLiteMigrationManager(db: db,
migrations: DatabaseConnection.migrations(),
bundle: DatabaseConnection.migrationsBundle())
Expand All @@ -44,6 +45,12 @@ class DatabaseConnection: NSObject {

@objc
func migrateIfNeeded() throws {
guard let migrationManager else {
throw NSError(domain: "DatabaseConnection", code: 1, userInfo: [
NSLocalizedDescriptionKey: "The wallet database could not be opened."
])
}

if !migrationManager.hasMigrationsTable() {
try migrationManager.createMigrationsTable()
}
Expand All @@ -62,11 +69,8 @@ extension DatabaseConnection {
let dirPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let docsDir = dirPaths[0] as String

guard let documentsURL = URL(string: docsDir) else {
fatalError("could not get user documents directory URL")
}

return documentsURL.appendingPathComponent(kDatabaseName)
return URL(fileURLWithPath: docsDir, isDirectory: true)
.appendingPathComponent(kDatabaseName)
}

static func migrations() -> [Migration] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ final class SwiftDashSDKKeyMigrator: NSObject {
network: Network,
isImported: Bool
) throws -> Data {
guard !Thread.isMainThread else {
throw MigrationError.hostCreateOnMainThread
}

let semaphore = DispatchSemaphore(value: 0)
var result: Result<Data, Error>?

Expand All @@ -228,6 +232,7 @@ final class SwiftDashSDKKeyMigrator: NSObject {

private enum MigrationError: LocalizedError {
case hostCreateDidNotReturn
case hostCreateOnMainThread
}

// MARK: - Helpers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ public final class SwiftDashSDKSPVCoordinator: NSObject, ObservableObject {
gapLimit: CoinJoinRecovery.recoveryGapLimit)
coinJoinRecoveryWidenedNetwork = network
Self.logger.info(
"🛰️ SPVCOORD :: CJTEST coinjoin recovery gap widened on \(network.rawValue, privacy: .public) to \(CoinJoinRecovery.recoveryGapLimit, privacy: .public)")
"🛰️ SPVCOORD :: coinjoin recovery gap widened on \(network.rawValue, privacy: .public) to \(CoinJoinRecovery.recoveryGapLimit, privacy: .public)")
} catch {
Self.logger.error(
"🛰️ SPVCOORD :: coinjoin recovery widen failed: \(String(describing: error), privacy: .public)")
Expand Down Expand Up @@ -452,7 +452,7 @@ public final class SwiftDashSDKSPVCoordinator: NSObject, ObservableObject {
let network = coinJoinRecoveryWidenedNetwork,
network == runningNetwork else { return }

Self.logger.info("🛰️ SPVCOORD :: CJTEST coinjoin recovery scan reached .synced on \(network.rawValue, privacy: .public) — marking recovered")
Self.logger.info("🛰️ SPVCOORD :: coinjoin recovery scan reached .synced on \(network.rawValue, privacy: .public) — marking recovered")
CoinJoinRecovery.shared.markRecovered(for: network)
coinJoinRecoveryWidenedNetwork = nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ final class SwiftDashSDKWalletCreator: NSObject {
network: Network,
isImported: Bool
) throws -> Data {
guard !Thread.isMainThread else {
throw CreateError.hostCreateOnMainThread
}

let semaphore = DispatchSemaphore(value: 0)
var result: Result<Data, Error>?

Expand All @@ -179,5 +183,6 @@ final class SwiftDashSDKWalletCreator: NSObject {

private enum CreateError: LocalizedError {
case hostCreateDidNotReturn
case hostCreateOnMainThread
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ final class SwiftDashSDKWalletWiper: NSObject {
private static func deleteWalletsFromSDK(
_ storedWalletIdsByNetwork: [Network: Set<Data>]
) -> Bool {
// `finished.wait()` below blocks this thread until the `@MainActor`
// task signals it. On the main thread that task could never be
// scheduled, so the wait would deadlock rather than fail.
guard !Thread.isMainThread else {
logger.error("Refusing synchronous wallet deletion on the main thread")
return false
}

let finished = DispatchSemaphore(value: 0)
let result = WalletWipeResultAccumulator()
Task { @MainActor in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ extension CBAccount {
idem: UUID?) async throws -> (transaction: CoinbaseTransaction, walletId: Data) {
// NOTE: Maybe better to get the address once and use it during the tx flow
guard let destination = SwiftDashSDKReceiveAddressReader.receiveDestination() else {
fatalError("No wallet")
throw Coinbase.Error.unknownError
}

// TODO: disabled until Coinbase changes are clear
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ struct CoinbaseUserAccountData: Codable, Identifiable {
var iconURL: URL {
let code = currency.code.lowercased()
let urlString = "https://raw.githubusercontent.com/jsupa/crypto-icons/main/icons/\(code).png"
return URL(string: urlString)!
return URL(string: urlString) ?? URL(fileURLWithPath: "/")
}

var balanceString: String {
Expand Down Expand Up @@ -68,7 +68,7 @@ struct CoinbaseUserAccountData: Codable, Identifiable {
}

let nf = NumberFormatter.fiatFormatter(currencyCode: App.fiatCurrency)
return nf.string(from: fiatAmount as NSNumber)!
return nf.string(from: fiatAmount as NSNumber) ?? "—"
}

var plainAmount: UInt64 {
Expand All @@ -83,7 +83,8 @@ struct CoinbaseUserAccountData: Codable, Identifiable {
var plainAmountInDash: UInt64 {
if currencyCode == kDashCurrency { return plainAmount }

guard let dashAmount = try? Coinbase.shared.currencyExchanger.convertToDash(amount: balance.amount.decimal()!, currency: currencyCode) else {
guard let amount = balance.amount.decimal(),
let dashAmount = try? Coinbase.shared.currencyExchanger.convertToDash(amount: amount, currency: currencyCode) else {
return 0
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ extension Amount {
assert(currency != kDashCurrency)

guard let decimal = amount.decimal() else {
fatalError("Trying to convert non number string")
return "—"
}

let numberFormatter = NumberFormatter.fiatFormatter(currencyCode: currency)

guard let string = numberFormatter.string(from: decimal as NSNumber) else {
fatalError("Trying to convert non number string")
return "—"
}

return string
Expand All @@ -35,11 +35,11 @@ extension Amount {
assert(currency == kDashCurrency)

guard let decimal = amount.decimal() else {
fatalError("Trying to convert non number string")
return "—"
}

guard let string = NumberFormatter.dashFormatter.string(from: decimal as NSNumber) else {
fatalError("Trying to convert non number string")
return "—"
}

return string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ extension CoinbaseRatesProvider {

private func fetchPrices() {
Task {
do {
let response: BaseDataResponse<CoinbaseExchangeRate> = try await httpClient.request(.exchangeRates(kDashCurrency))
guard let rates = response.data.rates else { return }

Expand All @@ -53,11 +54,14 @@ extension CoinbaseRatesProvider {

for rate in rates {
let key = rate.key
let price = Decimal(string: rate.value)!
guard let price = Decimal(string: rate.value) else { continue }
array.append(.init(code: key, name: key, price: price))
}

self.updateHandler?(array)
} catch {
DWLogger.log("Coinbase exchange rates request failed: \(error.localizedDescription)")
}
}
}
}
10 changes: 5 additions & 5 deletions DashWallet/Sources/Models/Transactions/WalletSendService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ final class WalletSendService: NSObject {
)
}

Self.logger.info("💸 TXSEND :: CJTEST preparing CoinJoin sweep — balance \(amount, privacy: .public) duffs (\(Double(amount) / 1e8, privacy: .public) DASH)")
Self.logger.info("💸 TXSEND :: preparing CoinJoin sweep — balance \(amount, privacy: .public) duffs (\(Double(amount) / 1e8, privacy: .public) DASH)")
try await sendAuthorizer.authorizeSend(spendAmount: amount)

guard let destination = SwiftDashSDKReceiveAddressReader.receiveAddress() else {
Expand All @@ -343,13 +343,13 @@ final class WalletSendService: NSObject {
)
}

Self.logger.info("💸 TXSEND :: CJTEST CoinJoin sweep destination resolved \(destination, privacy: .public)")
Self.logger.info("💸 TXSEND :: CoinJoin sweep destination resolved \(destination, privacy: .public)")
let txids = try SwiftDashSDKTransactionSender.sweepCoinJoin(to: destination)
guard !txids.isEmpty else {
// A reported-success sweep that produced no transaction is treated
// as a failure, so the caller surfaces an error (the sweep alert)
// rather than silently "succeeding" with the balance unchanged.
Self.logger.error("💸 TXSEND :: CJTEST CoinJoin sweep returned no transactions for \(amount, privacy: .public) duffs — treating as failure")
Self.logger.error("💸 TXSEND :: CoinJoin sweep returned no transactions for \(amount, privacy: .public) duffs — treating as failure")
throw Self.makeError(
code: .coinJoinSweepUnavailable,
description: "CoinJoin sweep produced no transactions"
Expand All @@ -366,12 +366,12 @@ final class WalletSendService: NSObject {
let recordedHexes: [String] = txids.map { (txid: Data) in
txid.reversed().map { String(format: "%02x", $0) }.joined()
}
Self.logger.info("💸 TXSEND :: CJTEST recorded \(txids.count, privacy: .public) sweep txid(s) in CoinJoinWithdrawalStore: \(recordedHexes.joined(separator: ","), privacy: .public)")
Self.logger.info("💸 TXSEND :: recorded \(txids.count, privacy: .public) sweep txid(s) in CoinJoinWithdrawalStore: \(recordedHexes.joined(separator: ","), privacy: .public)")

await MainActor.run {
SwiftDashSDKWalletState.shared.refreshCoinJoinBalance()
let post = SwiftDashSDKWalletState.shared.coinJoinBalanceDuffs
Self.logger.info("💸 TXSEND :: CJTEST post-sweep CoinJoin balance \(post, privacy: .public) duffs (was \(amount, privacy: .public))")
Self.logger.info("💸 TXSEND :: post-sweep CoinJoin balance \(post, privacy: .public) duffs (was \(amount, privacy: .public))")
// The per-network recovery flag is owned solely by the recovery scan-
// completion path (SwiftDashSDKSPVCoordinator.maybeCompleteCoinJoinRecovery,
// which marks recovered once the one-time wide scan reaches .synced). A
Expand Down
3 changes: 1 addition & 2 deletions DashWallet/Sources/UI/Coinbase/Base/ViewModel+Coinbase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,9 @@ extension CoinbaseTransactionSendable {
}
} catch {
await MainActor.run {
self.transactionDelegate?.transferFromCoinbaseToWalletDidFail(with: error as! Coinbase.Error)
self.transactionDelegate?.transferFromCoinbaseToWalletDidFail(with: error as? Coinbase.Error ?? .unknownError)
}
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,22 @@ extension ConvertCryptoOrderPreviewModel {
case .origin, .purchaseAmount:
let formatter = NumberFormatter.cryptoFormatter(currencyCode: selectedAccount.info.currencyCode, exponent: selectedAccount.info.currency.exponent)
formatter.minimumFractionDigits = 1
value = formatter.string(from: Decimal(string: order.inputAmount.amount)! as NSDecimalNumber) ?? "NaN"
guard let amount = Decimal(string: order.inputAmount.amount) else { return "—" }
value = formatter.string(from: amount as NSDecimalNumber) ?? "—"
case .destination:
let formatter = NumberFormatter.dashFormatter
value = formatter.string(from: Decimal(string: order.outputAmount.amount)! as NSDecimalNumber) ?? "NaN"
guard let amount = Decimal(string: order.outputAmount.amount) else { return "—" }
value = formatter.string(from: amount as NSDecimalNumber) ?? "—"
case .feeAmount:
value = order.fee.formattedFiatAmount
case .totalAmount:
let total = Decimal(string: order.fee.amount)! + Decimal(string: order.displayInputAmount.amount)!
guard let fee = Decimal(string: order.fee.amount),
let input = Decimal(string: order.displayInputAmount.amount) else { return "—" }
let total = fee + input
let numberFormatter = NumberFormatter.fiatFormatter(currencyCode: order.unitPrice.targetToFiat.currency)

guard let string = numberFormatter.string(from: total as NSNumber) else {
fatalError("Trying to convert non number string")
return "—"
}

value = string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ final class TransferAmountModel: CoinbaseAmountModel, CoinbaseTransactionSendabl
}
} catch let error {
await MainActor.run {
self.delegate?.transferFromCoinbaseToWalletDidFail(with: error as! Coinbase.Error)
self.delegate?.transferFromCoinbaseToWalletDidFail(with: error as? Coinbase.Error ?? .unknownError)
}
}
}
Expand Down
Loading
Loading