Skip to content

Commit

Permalink
Merge pull request #267 from litecoin-foundation/bugfix/issues-13-16
Browse files Browse the repository at this point in the history
Fixed NSInternalInconsistencyException & Swift runtime failure: arithmetic overflow
  • Loading branch information
azisramdhan authored Dec 27, 2024
2 parents 2817dee + 8b559a1 commit 670c800
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 66 deletions.
4 changes: 4 additions & 0 deletions litewallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
75A2A8101DA5936F00A983D8 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 75A2A80E1DA5936F00A983D8 /* MainInterface.storyboard */; };
75A2A8141DA5936F00A983D8 /* TodayExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 75A2A8081DA5936F00A983D8 /* TodayExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
75C735AA1DAA1B9C00251ECF /* libunbound.c in Sources */ = {isa = PBXBuildFile; fileRef = 755CD4121DAA0E3E0075898E /* libunbound.c */; };
9394D6482D1B424100E60FD6 /* UInt64+SafeArithmetic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9394D6472D1B424100E60FD6 /* UInt64+SafeArithmetic.swift */; };
C30029E225D0185500F08C2B /* StandardDividerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C30029E125D0185500F08C2B /* StandardDividerView.swift */; };
C30029EB25D019BC00F08C2B /* CopyButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C30029EA25D019BC00F08C2B /* CopyButtonView.swift */; };
C3019EE32B8FEFED00FAF648 /* AssociatedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3019EE22B8FEFED00FAF648 /* AssociatedObject.swift */; };
Expand Down Expand Up @@ -1389,6 +1390,7 @@
75A2A87C1DA59E4E00A983D8 /* litewallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = litewallet.entitlements; sourceTree = "<group>"; };
75C735AF1DAA1C9F00251ECF /* libnettle.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libnettle.a; sourceTree = BUILT_PRODUCTS_DIR; };
75FEFD1B1DAED56E00203D3A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };
9394D6472D1B424100E60FD6 /* UInt64+SafeArithmetic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt64+SafeArithmetic.swift"; sourceTree = "<group>"; };
C30029E125D0185500F08C2B /* StandardDividerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardDividerView.swift; sourceTree = "<group>"; };
C30029EA25D019BC00F08C2B /* CopyButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyButtonView.swift; sourceTree = "<group>"; };
C3019EE22B8FEFED00FAF648 /* AssociatedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociatedObject.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3258,6 +3260,7 @@
584E24F42951D2DB005E0E8B /* BundleExtension.swift */,
584E24FB2951D476005E0E8B /* NSNotificationNameExtension.swift */,
584E24FD2951D752005E0E8B /* UITableViewExtension.swift */,
9394D6472D1B424100E60FD6 /* UInt64+SafeArithmetic.swift */,
);
name = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -4190,6 +4193,7 @@
CE4CA7BC1EE3649100373F11 /* BRActivityView.swift in Sources */,
CEC6AA421DEFC88F00EE5AFD /* ReceiveViewController.swift in Sources */,
CEE20C2D1EA288FA0086F724 /* UpdatingLabel.swift in Sources */,
9394D6482D1B424100E60FD6 /* UInt64+SafeArithmetic.swift in Sources */,
CE3D4C591EF743EF0016B1C8 /* Functions.swift in Sources */,
CEE20C341EA5B4550086F724 /* ArticleIds.swift in Sources */,
CEE65DF01E39056F0002994D /* Rate.swift in Sources */,
Expand Down
42 changes: 42 additions & 0 deletions litewallet/UInt64+SafeArithmetic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// UInt64+SafeArithmetic.swift
// litewallet
//
// Created by Azis Ramdhan on 25/12/24.
// Copyright © 2024 Litecoin Foundation. All rights reserved.
//

import Foundation

extension UInt64 {
/// Adds two `UInt64` values safely, returning `nil` if overflow occurs.
///
/// This method is useful when you want to perform arithmetic operations
/// on `UInt64` values but need to ensure that the result stays within
/// the representable range of `UInt64`. Instead of causing a runtime crash
/// due to overflow, this method gracefully handles the overflow case by
/// returning `nil`.
///
/// - Parameter value: The value to add to the current `UInt64`.
/// - Returns: The sum of the two values, or `nil` if the operation results in an overflow.
func safeAddition(_ value: UInt64) -> UInt64? {
let (result, overflow) = addingReportingOverflow(value)
return overflow ? nil : result
}

/// Subtracts a `UInt64` value safely, returning `nil` if underflow occurs.
///
/// This method is useful when you want to perform subtraction on `UInt64`
/// values but need to ensure that the result does not go below zero (since
/// `UInt64` cannot represent negative numbers). Instead of causing a
/// runtime crash due to underflow, this method gracefully handles the
/// underflow case by returning `nil`.
///
/// - Parameter value: The value to subtract from the current `UInt64`.
/// - Returns: The result of the subtraction, or `nil` if the operation results in an underflow.
func safeSubtraction(_ value: UInt64) -> UInt64? {
let (result, underflow) = subtractingReportingOverflow(value)
return underflow ? nil : result
}
}

122 changes: 67 additions & 55 deletions litewallet/ViewModels/Transaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,61 +14,73 @@ class Transaction {

private let opsAddressSet: Set<String> = Partner.litewalletOpsSet()
/// Hassan
init?(_ tx: BRTxRef, walletManager: WalletManager, kvStore: BRReplicatedKVStore?, rate: Rate?) {
guard let wallet = walletManager.wallet else { return nil }
guard let peerManager = walletManager.peerManager else { return nil }

self.tx = tx
self.wallet = wallet
self.kvStore = kvStore
let fee = wallet.feeForTx(tx) ?? 0

var outputAddresses = Set<String>()
var opsAmount = UInt64(0)

for (_, output) in tx.outputs.enumerated() {
outputAddresses.insert(output.updatedSwiftAddress)
}

let outputAddress = opsAddressSet.intersection(outputAddresses).first
if let targetAddress = outputAddress,
let opsOutput = tx.outputs.filter({ $0.updatedSwiftAddress == targetAddress }).first
{
opsAmount = opsOutput.amount
}

self.fee = fee + opsAmount

let amountReceived = wallet.amountReceivedFromTx(tx)
let amountSent = wallet.amountSentByTx(tx) - opsAmount

if amountSent > 0, (amountReceived + fee) == amountSent {
direction = .moved
satoshis = amountSent
} else if amountSent > 0 {
direction = .sent
satoshis = amountSent - amountReceived - fee
} else {
direction = .received
satoshis = amountReceived
}
timestamp = Int(tx.pointee.timestamp)

isValid = wallet.transactionIsValid(tx)
let transactionBlockHeight = tx.pointee.blockHeight
self.blockHeight = tx.pointee.blockHeight == UInt32(INT32_MAX) ? S.TransactionDetails.notConfirmedBlockHeightLabel.localize() : "\(tx.pointee.blockHeight)"

let blockHeight = peerManager.lastBlockHeight
confirms = transactionBlockHeight > blockHeight ? 0 : Int(blockHeight - transactionBlockHeight) + 1
status = makeStatus(tx, wallet: wallet, peerManager: peerManager, confirms: confirms, direction: direction)

hash = tx.pointee.txHash.description
metaDataKey = tx.pointee.txHash.txKey

if let rate = rate, confirms < 6, direction == .received {
attemptCreateMetaData(tx: tx, rate: rate)
}
}
init?(_ tx: BRTxRef, walletManager: WalletManager, kvStore: BRReplicatedKVStore?, rate: Rate?) {
guard let wallet = walletManager.wallet else { return nil }
guard let peerManager = walletManager.peerManager else { return nil }

self.tx = tx
self.wallet = wallet
self.kvStore = kvStore
let fee = wallet.feeForTx(tx) ?? 0

var outputAddresses = Set<String>()
var opsAmount = UInt64(0)

for (_, output) in tx.outputs.enumerated() {
outputAddresses.insert(output.updatedSwiftAddress)
}

let outputAddress = opsAddressSet.intersection(outputAddresses).first
if let targetAddress = outputAddress,
let opsOutput = tx.outputs.filter({ $0.updatedSwiftAddress == targetAddress }).first
{
opsAmount = opsOutput.amount
}

// Ensure the total fee calculation is safe
guard let totalFee = fee.safeAddition(opsAmount) else { return nil }
self.fee = totalFee

let cachedFee = totalFee

let amountReceived = wallet.amountReceivedFromTx(tx)

// Calculate the amount sent, ensuring no underflow occurs
guard let amountSentAfterOps = wallet.amountSentByTx(tx).safeSubtraction(opsAmount) else { return nil }

// Verify total (amountReceived + fee) is within bounds
guard let totalReceivedAndFee = amountReceived.safeAddition(cachedFee) else { return nil }

if amountSentAfterOps > 0, amountSentAfterOps == totalReceivedAndFee {
direction = .moved
satoshis = amountSentAfterOps
} else if amountSentAfterOps > 0 {
// Deduct received amount and fee from sent amount safely
guard let intermediateSatoshis = amountSentAfterOps.safeSubtraction(amountReceived),
let finalSatoshis = intermediateSatoshis.safeSubtraction(fee) else { return nil }
direction = .sent
satoshis = finalSatoshis
} else {
direction = .received
satoshis = amountReceived
}
timestamp = Int(tx.pointee.timestamp)

isValid = wallet.transactionIsValid(tx)
let transactionBlockHeight = tx.pointee.blockHeight
self.blockHeight = tx.pointee.blockHeight == UInt32(INT32_MAX) ? S.TransactionDetails.notConfirmedBlockHeightLabel.localize() : "\(tx.pointee.blockHeight)"

let blockHeight = peerManager.lastBlockHeight
confirms = transactionBlockHeight > blockHeight ? 0 : Int(blockHeight - transactionBlockHeight) + 1
status = makeStatus(tx, wallet: wallet, peerManager: peerManager, confirms: confirms, direction: direction)

hash = tx.pointee.txHash.description
metaDataKey = tx.pointee.txHash.txKey

if let rate = rate, confirms < 6, direction == .received {
attemptCreateMetaData(tx: tx, rate: rate)
}
}

func amountDescription(isLtcSwapped: Bool, rate: Rate, maxDigits: Int) -> String {
let amount = Amount(amount: satoshis, rate: rate, maxDigits: maxDigits)
Expand Down
29 changes: 18 additions & 11 deletions litewallet/Views/DefaultCurrencyViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,11 @@ class DefaultCurrencyViewController: UITableViewController, Subscriber {
}
}

private var defaultCurrencyCode: String? {
didSet {
// Grab index paths of new and old rows when the currency changes
let paths: [IndexPath] = rates.enumerated().filter { $0.1.code == defaultCurrencyCode || $0.1.code == oldValue }.map { IndexPath(row: $0.0, section: 0) }
tableView.beginUpdates()
tableView.reloadRows(at: paths, with: .automatic)
tableView.endUpdates()

setExchangeRateLabel()
}
}
private var defaultCurrencyCode: String? {
didSet {
updateCurrencyRows(oldCurrencyCode: oldValue)
}
}

private let bitcoinLabel = UILabel(font: .customBold(size: 14.0), color: .grayTextTint)
private let bitcoinSwitch = UISegmentedControl(items: ["photons (\(S.Symbols.photons))", "lites (\(S.Symbols.lites))", "LTC (\(S.Symbols.ltc))"])
Expand Down Expand Up @@ -72,6 +66,19 @@ class DefaultCurrencyViewController: UITableViewController, Subscriber {
rateLabel.text = "\(bitsAmount.bits) = \(amount.string(forLocal: currentRate.locale))"
}
}

private func updateCurrencyRows(oldCurrencyCode: String?) {
// Grab index paths of new and old rows when the currency changes
let paths: [IndexPath] = rates.enumerated().filter { $0.1.code == defaultCurrencyCode || $0.1.code == oldCurrencyCode }.map { IndexPath(row: $0.0, section: 0) }

Task { @MainActor in
tableView.beginUpdates()
tableView.reloadRows(at: paths, with: .automatic)
tableView.endUpdates()

setExchangeRateLabel()
}
}

override func numberOfSections(in _: UITableView) -> Int {
return 1
Expand Down

0 comments on commit 670c800

Please sign in to comment.