diff --git a/loafwallet.xcodeproj/project.pbxproj b/loafwallet.xcodeproj/project.pbxproj index 997d0bd32..42acc600d 100644 --- a/loafwallet.xcodeproj/project.pbxproj +++ b/loafwallet.xcodeproj/project.pbxproj @@ -296,6 +296,15 @@ C3D783A72565EA4B0004FF70 /* UnstoppableDomainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D783A62565EA4A0004FF70 /* UnstoppableDomainView.swift */; }; C3D783B72565EA6B0004FF70 /* UnstoppableDomainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D783B62565EA6B0004FF70 /* UnstoppableDomainViewModel.swift */; }; C3D783C02565ECF60004FF70 /* UnstoppableDomainViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D783BF2565ECF60004FF70 /* UnstoppableDomainViewModelTests.swift */; }; + C3E751C22AF689BA005571CA /* BRKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E751C12AF689BA005571CA /* BRKeyExtension.swift */; }; + C3E751C42AF68A50005571CA /* BRAddressExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E751C32AF68A50005571CA /* BRAddressExtension.swift */; }; + C3E751C62AF68A8E005571CA /* BRTxInputExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E751C52AF68A8E005571CA /* BRTxInputExtension.swift */; }; + C3E751C82AF68AEB005571CA /* UnsafeMutablePointerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E751C72AF68AEB005571CA /* UnsafeMutablePointerExtension.swift */; }; + C3E751CB2AF68B47005571CA /* BRPeerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E751CA2AF68B47005571CA /* BRPeerManager.swift */; }; + C3E751CD2AF68B93005571CA /* BRWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E751CC2AF68B93005571CA /* BRWallet.swift */; }; + C3E751CF2AF68C18005571CA /* BRCalculationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E751CE2AF68C18005571CA /* BRCalculationExtension.swift */; }; + C3E751D12AF68C84005571CA /* BRMasterKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E751D02AF68C84005571CA /* BRMasterKeyExtension.swift */; }; + C3E751D32AF68CD1005571CA /* BRTxOutputExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E751D22AF68CD1005571CA /* BRTxOutputExtension.swift */; }; C3EFA9A12650807B005C59B5 /* LockScreenHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3EFA9A02650807B005C59B5 /* LockScreenHeaderView.swift */; }; C3EFA9A3265080FF005C59B5 /* LockScreenHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3EFA9A2265080FF005C59B5 /* LockScreenHeaderViewModel.swift */; }; C3EFA9A62651A808005C59B5 /* LockScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3EFA9A52651A808005C59B5 /* LockScreenTests.swift */; }; @@ -1453,6 +1462,15 @@ C3D783A62565EA4A0004FF70 /* UnstoppableDomainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstoppableDomainView.swift; sourceTree = ""; }; C3D783B62565EA6B0004FF70 /* UnstoppableDomainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstoppableDomainViewModel.swift; sourceTree = ""; }; C3D783BF2565ECF60004FF70 /* UnstoppableDomainViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstoppableDomainViewModelTests.swift; sourceTree = ""; }; + C3E751C12AF689BA005571CA /* BRKeyExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRKeyExtension.swift; sourceTree = ""; }; + C3E751C32AF68A50005571CA /* BRAddressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRAddressExtension.swift; sourceTree = ""; }; + C3E751C52AF68A8E005571CA /* BRTxInputExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRTxInputExtension.swift; sourceTree = ""; }; + C3E751C72AF68AEB005571CA /* UnsafeMutablePointerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsafeMutablePointerExtension.swift; sourceTree = ""; }; + C3E751CA2AF68B47005571CA /* BRPeerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRPeerManager.swift; sourceTree = ""; }; + C3E751CC2AF68B93005571CA /* BRWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRWallet.swift; sourceTree = ""; }; + C3E751CE2AF68C18005571CA /* BRCalculationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRCalculationExtension.swift; sourceTree = ""; }; + C3E751D02AF68C84005571CA /* BRMasterKeyExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRMasterKeyExtension.swift; sourceTree = ""; }; + C3E751D22AF68CD1005571CA /* BRTxOutputExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRTxOutputExtension.swift; sourceTree = ""; }; C3EFA9A02650807B005C59B5 /* LockScreenHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenHeaderView.swift; sourceTree = ""; }; C3EFA9A2265080FF005C59B5 /* LockScreenHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenHeaderViewModel.swift; sourceTree = ""; }; C3EFA9A52651A808005C59B5 /* LockScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenTests.swift; sourceTree = ""; }; @@ -2932,6 +2950,8 @@ 75A2A7931DA5934300A983D8 /* AppDelegate.swift */, CE20C8F11DBAF71500C8397A /* ApplicationController.swift */, 754AE0BB1DFE8A46007FD001 /* BRCore.swift */, + C3E751C92AF68B2F005571CA /* BRCoreClasses */, + C3E751C02AF689A0005571CA /* BRExtensions */, 75519F331DC7D20500EDF66C /* WalletManager.swift */, 7503773C1DF57428005EB8AE /* WalletManager+Auth.swift */, CE4B6C191E219CA600CF935B /* WalletCoordinator.swift */, @@ -3111,6 +3131,29 @@ name = Unstoppable; sourceTree = ""; }; + C3E751C02AF689A0005571CA /* BRExtensions */ = { + isa = PBXGroup; + children = ( + C3E751C12AF689BA005571CA /* BRKeyExtension.swift */, + C3E751C32AF68A50005571CA /* BRAddressExtension.swift */, + C3E751C52AF68A8E005571CA /* BRTxInputExtension.swift */, + C3E751C72AF68AEB005571CA /* UnsafeMutablePointerExtension.swift */, + C3E751CE2AF68C18005571CA /* BRCalculationExtension.swift */, + C3E751D02AF68C84005571CA /* BRMasterKeyExtension.swift */, + C3E751D22AF68CD1005571CA /* BRTxOutputExtension.swift */, + ); + name = BRExtensions; + sourceTree = ""; + }; + C3E751C92AF68B2F005571CA /* BRCoreClasses */ = { + isa = PBXGroup; + children = ( + C3E751CA2AF68B47005571CA /* BRPeerManager.swift */, + C3E751CC2AF68B93005571CA /* BRWallet.swift */, + ); + name = BRCoreClasses; + sourceTree = ""; + }; C3EFA9A42651A7C4005C59B5 /* Lock Screen Tests */ = { isa = PBXGroup; children = ( @@ -3886,24 +3929,6 @@ shellPath = /bin/sh; shellScript = "cd BuildTools\nSDKROOT=(xcrun --sdk macosx --show-sdk-path)\n#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\"\n"; }; - 583FE8702927ED5A009A6384 /* Run SwiftFormat */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Run SwiftFormat"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "cd BuildTools\nSDKROOT=(xcrun --sdk macosx --show-sdk-path)\n#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\"\n"; - }; 58A9FE4829191E4700B75825 /* Check for unused code */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -4126,6 +4151,7 @@ C3D783B72565EA6B0004FF70 /* UnstoppableDomainViewModel.swift in Sources */, CEC6AA3B1DEE4EB000EE5AFD /* CGRect+Additions.swift in Sources */, CE45C1FB1E74F89C002C3847 /* WalletInfo.swift in Sources */, + C3E751D12AF68C84005571CA /* BRMasterKeyExtension.swift in Sources */, C3FF4D5F28AC5A5800713139 /* SendAddressCellView.swift in Sources */, C36375A328BD38A500CFB3D8 /* SendButtonHostingController.swift in Sources */, CEE659E71F65A936001FF29D /* RetryTimer.swift in Sources */, @@ -4148,6 +4174,7 @@ CEE1F5631DF13E5A00D733AD /* ModalHeaderView.swift in Sources */, CE5F21DB1E4A93A500C47B8E /* LoginTransitionDelegate.swift in Sources */, 584E24FA2951D43A005E0E8B /* LanguageSelectionViewModel.swift in Sources */, + C3E751CB2AF68B47005571CA /* BRPeerManager.swift in Sources */, CEE659E91F664C73001FF29D /* WelcomeViewController.swift in Sources */, 24313C8723821B8C00A83F69 /* PromptTableViewCell.swift in Sources */, 2228734F1E916FC30044BA15 /* BRAPIClient+Wallet.swift in Sources */, @@ -4282,6 +4309,8 @@ CE6D0F991DE8B75900BD4BCF /* DismissModalAnimator.swift in Sources */, C39443F9269DDAD3002703E9 /* LitewalletIconView.swift in Sources */, CE4C1CC61ED65D830063E184 /* DrawableCircle.swift in Sources */, + C3E751CD2AF68B93005571CA /* BRWallet.swift in Sources */, + C3E751D32AF68CD1005571CA /* BRTxOutputExtension.swift in Sources */, CE8CD8E31E31978100785E02 /* LoginBackgroundTriangle.swift in Sources */, C35ABD332574073F002BB9BB /* PartnersViewModel.swift in Sources */, CEEC708C1E95461A00EF788E /* AboutViewController.swift in Sources */, @@ -4305,6 +4334,7 @@ CEF61B121ECF52C700C7EA6A /* AmountViewController.swift in Sources */, CECCE5B21E04B00D00D99448 /* SendCell.swift in Sources */, 24B8FAD22162B10200A155B1 /* BuyCenterWebViewController.swift in Sources */, + C3E751C62AF68A8E005571CA /* BRTxInputExtension.swift in Sources */, CE6DCC301E6666470044257B /* NonScrollingCollectionView.swift in Sources */, CE20C9011DBBFFF800C8397A /* Actions.swift in Sources */, CE20C9191DBE7B8200C8397A /* ReduxState.swift in Sources */, @@ -4318,6 +4348,7 @@ C3D783A72565EA4B0004FF70 /* UnstoppableDomainView.swift in Sources */, CE3D4C571EF5D5740016B1C8 /* ReachabilityMonitor.swift in Sources */, C32142FA25C988C800BECCD0 /* TransactionCellViewModel.swift in Sources */, + C3E751C22AF689BA005571CA /* BRKeyExtension.swift in Sources */, CE4DFB2E1E9C26DA0014009E /* ShareDataViewController.swift in Sources */, CE1E5F261EF083A600BD0F72 /* StartImportViewController.swift in Sources */, CE25BF931DFDA7A600BC67B6 /* MessageUIPresenter.swift in Sources */, @@ -4332,16 +4363,19 @@ CE5F21D91E4A922700C47B8E /* DismissLoginAnimator.swift in Sources */, CECCE5A51E02408300D99448 /* UIView+FrameChangeBlocking.swift in Sources */, CE6DCC271E6108D50044257B /* EnterPhraseCell.swift in Sources */, + C3E751CF2AF68C18005571CA /* BRCalculationExtension.swift in Sources */, C3EFA9A3265080FF005C59B5 /* LockScreenHeaderViewModel.swift in Sources */, CE760EDD1E561DF900EFAC2B /* UpdatePinViewController.swift in Sources */, 584E24F82951D412005E0E8B /* LanguageSelectionViewController.swift in Sources */, 24BA90C62410129E001E3825 /* FeeSelectorView.swift in Sources */, CE9057181DFF0FA8006BA848 /* String+Additions.swift in Sources */, CED341331EF5A5C00014912A /* InAppAlert.swift in Sources */, + C3E751C42AF68A50005571CA /* BRAddressExtension.swift in Sources */, CE47A8E01F7DA54000FF35BA /* UIScreen+Additions.swift in Sources */, 222C42521E904C5000078EB5 /* AssociatedObject.swift in Sources */, CEB909F51E5FE63D001804DC /* EnterPhraseViewController.swift in Sources */, CE6D0F971DE8B73A00BD4BCF /* ModalTransitionDelegate.swift in Sources */, + C3E751C82AF68AEB005571CA /* UnsafeMutablePointerExtension.swift in Sources */, C3BD4A5325975C6000D97079 /* View+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/loafwallet/BRAddressExtension.swift b/loafwallet/BRAddressExtension.swift new file mode 100644 index 000000000..55d934cda --- /dev/null +++ b/loafwallet/BRAddressExtension.swift @@ -0,0 +1,60 @@ +// +// BRAddressExtension.swift +// loafwallet +// +// Created by Kerry Washington on 11/4/23. +// Copyright © 2023 Litecoin Foundation. All rights reserved. +// +import BRCore +import Foundation + +extension BRAddress: CustomStringConvertible, Hashable { + init?(string: String) { + self.init() + let cStr = [CChar](string.utf8CString) + guard cStr.count <= MemoryLayout.size else { return nil } + UnsafeMutableRawPointer(mutating: &s).assumingMemoryBound(to: CChar.self).assign(from: cStr, + count: cStr.count) + } + + init?(scriptPubKey: [UInt8]) { + self.init() + guard BRAddressFromScriptPubKey(UnsafeMutableRawPointer(mutating: &s).assumingMemoryBound(to: CChar.self), + MemoryLayout.size, scriptPubKey, scriptPubKey.count) > 0 + else { return nil } + } + + init?(scriptSig: [UInt8]) { + self.init() + guard BRAddressFromScriptSig(UnsafeMutableRawPointer(mutating: &s).assumingMemoryBound(to: CChar.self), + MemoryLayout.size, scriptSig, scriptSig.count) > 0 else { return nil } + } + + var scriptPubKey: [UInt8]? { + var script = [UInt8](repeating: 0, count: 25) + let count = BRAddressScriptPubKey(&script, script.count, + UnsafeRawPointer([s]).assumingMemoryBound(to: CChar.self)) + guard count > 0 else { return nil } + if count < script.count { script.removeSubrange(count...) } + return script + } + + var hash160: UInt160? { + var hash = UInt160() + guard BRAddressHash160(&hash, UnsafeRawPointer([s]).assumingMemoryBound(to: CChar.self)) != 0 + else { return nil } + return hash + } + + public var description: String { + return String(cString: UnsafeRawPointer([s]).assumingMemoryBound(to: CChar.self)) + } + + public var hashValue: Int { + return BRAddressHash([s]) + } + + public static func == (l: BRAddress, r: BRAddress) -> Bool { + return BRAddressEq([l.s], [r.s]) != 0 + } +} diff --git a/loafwallet/BRCalculationExtension.swift b/loafwallet/BRCalculationExtension.swift new file mode 100644 index 000000000..87b10a51c --- /dev/null +++ b/loafwallet/BRCalculationExtension.swift @@ -0,0 +1,60 @@ +import BRCore +import Foundation + +extension UInt256: CustomStringConvertible { + public var description: String { + return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + u8.31, u8.30, u8.29, u8.28, u8.27, u8.26, u8.25, u8.24, + u8.23, u8.22, u8.21, u8.20, u8.19, u8.18, u8.17, u8.16, + u8.15, u8.14, u8.13, u8.12, u8.11, u8.10, u8.9, u8.8, + u8.7, u8.6, u8.5, u8.4, u8.3, u8.2, u8.1, u8.0) + } +} + +extension UInt128: Equatable { + public static func == (l: UInt128, r: UInt128) -> Bool { + return l.u64 == r.u64 + } + + public static func != (l: UInt128, r: UInt128) -> Bool { + return l.u64 != r.u64 + } +} + +extension UInt160: Equatable { + public static func == (l: UInt160, r: UInt160) -> Bool { + return l.u32 == r.u32 + } + + public static func != (l: UInt160, r: UInt160) -> Bool { + return l.u32 != r.u32 + } +} + +extension UInt256: Equatable { + public static func == (l: UInt256, r: UInt256) -> Bool { + return l.u64 == r.u64 + } + + public static func != (l: UInt256, r: UInt256) -> Bool { + return l.u64 != r.u64 + } + + var hexString: String { + var u = self + return withUnsafePointer(to: &u) { p in + Data(bytes: p, count: MemoryLayout.stride).hexString + } + } +} + +extension UInt512: Equatable { + public static func == (l: UInt512, r: UInt512) -> Bool { + return l.u64 == r.u64 + } + + public static func != (l: UInt512, r: UInt512) -> Bool { + return l.u64 != r.u64 + } +} diff --git a/loafwallet/BRKeyExtension.swift b/loafwallet/BRKeyExtension.swift new file mode 100644 index 000000000..d5d4e1408 --- /dev/null +++ b/loafwallet/BRKeyExtension.swift @@ -0,0 +1,118 @@ +// +// BRKeyExtension.swift +// loafwallet +// +// Created by Kerry Washington on 11/4/23. +// Copyright © 2023 Litecoin Foundation. All rights reserved. +// +import BRCore +import Foundation + +extension BRKey { + // privKey must be wallet import format (WIF), mini private key format, or hex string + init?(privKey: String) { + self.init() + guard BRKeySetPrivKey(&self, privKey) != 0 else { return nil } + } + + // decrypts a BIP38 key using the given passphrase and returns nil if passphrase is incorrect + init?(bip38Key: String, passphrase: String) { + self.init() + guard let nfcPhrase = CFStringCreateMutableCopy(secureAllocator, 0, passphrase as CFString) else { return nil } + CFStringNormalize(nfcPhrase, .C) // NFC unicode normalization + guard BRKeySetBIP38Key(&self, bip38Key, nfcPhrase as String) != 0 else { return nil } + } + + // pubKey must be a DER encoded public key + init?(pubKey: [UInt8]) { + self.init() + guard BRKeySetPubKey(&self, pubKey, pubKey.count) != 0 else { return nil } + } + + init?(secret: UnsafePointer, compact: Bool) { + self.init() + guard BRKeySetSecret(&self, secret, compact ? 1 : 0) != 0 else { return nil } + } + + // recover a pubKey from a compact signature + init?(md: UInt256, compactSig: [UInt8]) { + self.init() + guard BRKeyRecoverPubKey(&self, md, compactSig, compactSig.count) != 0 else { return nil } + } + + // WIF private key + mutating func privKey() -> String? { + return autoreleasepool + { // wrapping in autoreleasepool ensures sensitive memory is wiped and freed immediately + let count = BRKeyPrivKey(&self, nil, 0) + var data = CFDataCreateMutable(secureAllocator, count) as Data + data.count = count + guard data.withUnsafeMutableBytes({ BRKeyPrivKey(&self, $0, count) }) != 0 else { return nil } + return CFStringCreateFromExternalRepresentation(secureAllocator, data as CFData, + CFStringBuiltInEncodings.UTF8.rawValue) as String + } + } + + // encrypts key with passphrase + mutating func bip38Key(passphrase: String) -> String? { + return autoreleasepool { + guard let nfcPhrase = CFStringCreateMutableCopy(secureAllocator, 0, passphrase as CFString) + else { return nil } + CFStringNormalize(nfcPhrase, .C) // NFC unicode normalization + let count = BRKeyBIP38Key(&self, nil, 0, nfcPhrase as String) + var data = CFDataCreateMutable(secureAllocator, count) as Data + data.count = count + guard data.withUnsafeMutableBytes({ BRKeyBIP38Key(&self, $0, count, nfcPhrase as String) }) != 0 + else { return nil } + return CFStringCreateFromExternalRepresentation(secureAllocator, data as CFData, + CFStringBuiltInEncodings.UTF8.rawValue) as String + } + } + + // DER encoded public key + mutating func pubKey() -> [UInt8]? { + var pubKey = [UInt8](repeating: 0, count: BRKeyPubKey(&self, nil, 0)) + guard !pubKey.isEmpty, BRKeyPubKey(&self, &pubKey, pubKey.count) == pubKey.count else { return nil } + return pubKey + } + + // ripemd160 hash of the sha256 hash of the public key + mutating func hash160() -> UInt160? { + let hash = BRKeyHash160(&self) + guard hash != UInt160() else { return nil } + return hash + } + + // pay-to-pubkey-hash litecoin address + mutating func address() -> String? { + var addr = [CChar](repeating: 0, count: MemoryLayout.size) + guard BRKeyAddress(&self, &addr, addr.count) > 0 else { return nil } + return String(cString: addr) + } + + mutating func sign(md: UInt256) -> [UInt8]? { + var sig = [UInt8](repeating: 0, count: 73) + let count = BRKeySign(&self, &sig, sig.count, md) + guard count > 0 else { return nil } + if count < sig.count { sig.removeSubrange(sig.count...) } + return sig + } + + mutating func verify(md: UInt256, sig: [UInt8]) -> Bool { + var sig = sig + return BRKeyVerify(&self, md, &sig, sig.count) != 0 + } + + // wipes key material + mutating func clean() { + BRKeyClean(&self) + } + + // Pieter Wuille's compact signature encoding used for bitcoin message signing + // to verify a compact signature, recover a public key from the sig and verify that it matches the signer's pubkey + mutating func compactSign(md: UInt256) -> [UInt8]? { + var sig = [UInt8](repeating: 0, count: 65) + guard BRKeyCompactSign(&self, &sig, sig.count, md) == sig.count else { return nil } + return sig + } +} diff --git a/loafwallet/BRMasterKeyExtension.swift b/loafwallet/BRMasterKeyExtension.swift new file mode 100644 index 000000000..28accea93 --- /dev/null +++ b/loafwallet/BRMasterKeyExtension.swift @@ -0,0 +1,12 @@ +import BRCore +import Foundation + +extension BRMasterPubKey: Equatable { + public static func == (l: BRMasterPubKey, r: BRMasterPubKey) -> Bool { + return l.fingerPrint == r.fingerPrint && l.chainCode == r.chainCode && l.pubKey == r.pubKey + } + + public static func != (l: BRMasterPubKey, r: BRMasterPubKey) -> Bool { + return l.fingerPrint != r.fingerPrint || l.chainCode != r.chainCode || l.pubKey != r.pubKey + } +} diff --git a/loafwallet/BRPeerManager.swift b/loafwallet/BRPeerManager.swift new file mode 100644 index 000000000..f6d1853b5 --- /dev/null +++ b/loafwallet/BRPeerManager.swift @@ -0,0 +1,164 @@ +import BRCore +import Foundation + +class BRPeerManager { + let cPtr: OpaquePointer + let listener: BRPeerManagerListener + let mainNetParams = [BRMainNetParams] + var falsePositiveRate: Double + + init?(wallet: BRWallet, + earliestKeyTime: TimeInterval, + blocks: [BRBlockRef?], peers: [BRPeer], + listener: BRPeerManagerListener, + fpRate: Double) + { + var blockRefs = blocks + guard let cPtr = BRPeerManagerNew(mainNetParams, wallet.cPtr, UInt32(earliestKeyTime + NSTimeIntervalSince1970), + &blockRefs, blockRefs.count, peers, peers.count, fpRate) else { return nil } + self.listener = listener + self.cPtr = cPtr + falsePositiveRate = fpRate + + BRPeerManagerSetCallbacks(cPtr, Unmanaged.passUnretained(self).toOpaque(), + { info in // syncStarted + guard let info = info else { return } + Unmanaged.fromOpaque(info).takeUnretainedValue().listener.syncStarted() + }, + { info, error in // syncStopped + guard let info = info else { return } + let err = BRPeerManagerError.posixError(errorCode: error, description: String(cString: strerror(error))) + Unmanaged.fromOpaque(info).takeUnretainedValue().listener.syncStopped(error != 0 ? err : nil) + }, + { info in // txStatusUpdate + guard let info = info else { return } + Unmanaged.fromOpaque(info).takeUnretainedValue().listener.txStatusUpdate() + }, + { info, replace, blocks, blocksCount in // saveBlocks + guard let info = info else { return } + let blockRefs = [BRBlockRef?](UnsafeBufferPointer(start: blocks, count: blocksCount)) + Unmanaged.fromOpaque(info).takeUnretainedValue().listener.saveBlocks(replace != 0, blockRefs) + }, + { info, replace, peers, peersCount in // savePeers + guard let info = info else { return } + let peerList = [BRPeer](UnsafeBufferPointer(start: peers, count: peersCount)) + Unmanaged.fromOpaque(info).takeUnretainedValue().listener.savePeers(replace != 0, peerList) + }, + { info -> Int32 in // networkIsReachable + guard let info = info else { return 0 } + return Unmanaged.fromOpaque(info).takeUnretainedValue().listener.networkIsReachable() ? 1 : 0 + }, + nil) // threadCleanup + } + + // true if currently connected to at least one peer + var isConnected: Bool { + return BRPeerManagerConnectStatus(cPtr) == BRPeerStatusConnected + } + + // connect to bitcoin peer-to-peer network (also call this whenever networkIsReachable() status changes) + func connect() { + if let fixedAddress = UserDefaults.customNodeIP { + setFixedPeer(address: fixedAddress, port: UserDefaults.customNodePort ?? C.standardPort) + } + BRPeerManagerConnect(cPtr) + } + + // disconnect from bitcoin peer-to-peer network + func disconnect() { + BRPeerManagerDisconnect(cPtr) + } + + // rescans blocks and transactions after earliestKeyTime (a new random download peer is also selected due to the + // possibility that a malicious node might lie by omitting transactions that match the bloom filter) + func rescan() { + BRPeerManagerRescan(cPtr) + } + + // current proof-of-work verified best block height + var lastBlockHeight: UInt32 { + return BRPeerManagerLastBlockHeight(cPtr) + } + + // current proof-of-work verified best block timestamp (time interval since unix epoch) + var lastBlockTimestamp: UInt32 { + return BRPeerManagerLastBlockTimestamp(cPtr) + } + + // the (unverified) best block height reported by connected peers + var estimatedBlockHeight: UInt32 { + return BRPeerManagerEstimatedBlockHeight(cPtr) + } + + // Only show syncing view if more than 2 days behind + var shouldShowSyncingView: Bool { + let lastBlock = Date(timeIntervalSince1970: TimeInterval(lastBlockTimestamp)) + let cutoff = Date().addingTimeInterval(-24 * 60 * 60 * 2) // 2 days ago + return lastBlock.compare(cutoff) == .orderedAscending + } + + // current network sync progress from 0 to 1 + // startHeight is the block height of the most recent fully completed sync + func syncProgress(fromStartHeight: UInt32) -> Double { + return BRPeerManagerSyncProgress(cPtr, fromStartHeight) + } + + // the number of currently connected peers + var peerCount: Int { + return BRPeerManagerPeerCount(cPtr) + } + + // description of the peer most recently used to sync blockchain data + var downloadPeerName: String { + return String(cString: BRPeerManagerDownloadPeerName(cPtr)) + } + + // publishes tx to bitcoin network + func publishTx(_ tx: BRTxRef, completion: @escaping (Bool, BRPeerManagerError?) -> Void) { + BRPeerManagerPublishTx(cPtr, tx, Unmanaged.passRetained(CompletionWrapper(completion)).toOpaque()) + { info, error in + guard let info = info else { return } + guard error == 0 + else { + let err = BRPeerManagerError.posixError(errorCode: error, description: String(cString: strerror(error))) + return Unmanaged.fromOpaque(info).takeRetainedValue().completion(false, err) + } + + Unmanaged.fromOpaque(info).takeRetainedValue().completion(true, nil) + } + } + + // number of connected peers that have relayed the given unconfirmed transaction + func relayCount(_ forTxHash: UInt256) -> Int { + return BRPeerManagerRelayCount(cPtr, forTxHash) + } + + func setFixedPeer(address: Int, port: Int) { + if address != 0 { + var newAddress = UInt128() + newAddress.u16.5 = 0xFFFF + newAddress.u32.3 = UInt32(address) + BRPeerManagerSetFixedPeer(cPtr, newAddress, UInt16(port)) + } else { + BRPeerManagerSetFixedPeer(cPtr, UInt128(), 0) + } + } + + deinit { + BRPeerManagerDisconnect(cPtr) + BRPeerManagerFree(cPtr) + } + + private class CompletionWrapper { + let completion: (Bool, BRPeerManagerError?) -> Void + + init(_ completion: @escaping (Bool, BRPeerManagerError?) -> Void) { + self.completion = completion + } + } + + // hack to keep the swift compiler happy + let a = BRMainNetDNSSeeds + let b = BRMainNetCheckpoints + let c = BRMainNetVerifyDifficulty +} diff --git a/loafwallet/BRTxInputExtension.swift b/loafwallet/BRTxInputExtension.swift new file mode 100644 index 000000000..9ef47a293 --- /dev/null +++ b/loafwallet/BRTxInputExtension.swift @@ -0,0 +1,26 @@ +import BRCore +import Foundation + +extension BRTxInput { + var swiftAddress: String { + get { return String(cString: UnsafeRawPointer([address]).assumingMemoryBound(to: CChar.self)) } + set { BRTxInputSetAddress(&self, newValue) } + } + + var updatedSwiftAddress: String { + get { + return charInt8ToString(charArray: address) + } + set { BRTxInputSetAddress(&self, newValue) } + } + + var swiftScript: [UInt8] { + get { return [UInt8](UnsafeBufferPointer(start: script, count: scriptLen)) } + set { BRTxInputSetScript(&self, newValue, newValue.count) } + } + + var swiftSignature: [UInt8] { + get { return [UInt8](UnsafeBufferPointer(start: signature, count: sigLen)) } + set { BRTxInputSetSignature(&self, newValue, newValue.count) } + } +} diff --git a/loafwallet/BRTxOutputExtension.swift b/loafwallet/BRTxOutputExtension.swift new file mode 100644 index 000000000..9f0b44bb3 --- /dev/null +++ b/loafwallet/BRTxOutputExtension.swift @@ -0,0 +1,24 @@ +import BRCore +import Foundation + +extension BRTxOutput { + var swiftAddress: String { + get { + return + String(cString: UnsafeRawPointer([address]).assumingMemoryBound(to: CChar.self)) + } + set { BRTxOutputSetAddress(&self, newValue) } + } + + var updatedSwiftAddress: String { + get { + return charInt8ToString(charArray: address) + } + set { BRTxOutputSetAddress(&self, newValue) } + } + + var swiftScript: [UInt8] { + get { return [UInt8](UnsafeBufferPointer(start: script, count: scriptLen)) } + set { BRTxOutputSetScript(&self, newValue, newValue.count) } + } +} diff --git a/loafwallet/BRWallet.swift b/loafwallet/BRWallet.swift new file mode 100644 index 000000000..8ee70d197 --- /dev/null +++ b/loafwallet/BRWallet.swift @@ -0,0 +1,158 @@ +import BRCore +import Foundation + +class BRWallet { + let cPtr: OpaquePointer + let listener: BRWalletListener + + init?(transactions: [BRTxRef?], masterPubKey: BRMasterPubKey, listener: BRWalletListener) { + var txRefs = transactions + guard let cPtr = BRWalletNew(&txRefs, txRefs.count, masterPubKey) else { return nil } + self.listener = listener + self.cPtr = cPtr + + BRWalletSetCallbacks(cPtr, Unmanaged.passUnretained(self).toOpaque(), + { info, balance in // balanceChanged + guard let info = info else { return } + Unmanaged.fromOpaque(info).takeUnretainedValue().listener.balanceChanged(balance) + }, + { info, tx in // txAdded + guard let info = info, let tx = tx else { return } + Unmanaged.fromOpaque(info).takeUnretainedValue().listener.txAdded(tx) + }, + { info, txHashes, txCount, blockHeight, timestamp in // txUpdated + guard let info = info else { return } + let hashes = [UInt256](UnsafeBufferPointer(start: txHashes, count: txCount)) + Unmanaged.fromOpaque(info).takeUnretainedValue().listener.txUpdated(hashes, + blockHeight: blockHeight, + timestamp: timestamp) + }, + { info, txHash, notify, rescan in // txDeleted + guard let info = info else { return } + Unmanaged.fromOpaque(info).takeUnretainedValue().listener.txDeleted(txHash, + notifyUser: notify != 0, + recommendRescan: rescan != 0) + }) + } + + // the first unused external address + var receiveAddress: String { + return BRWalletReceiveAddress(cPtr).description + } + + // all previously genereated internal and external addresses + var allAddresses: [String] { + var addrs = [BRAddress](repeating: BRAddress(), count: BRWalletAllAddrs(cPtr, nil, 0)) + guard BRWalletAllAddrs(cPtr, &addrs, addrs.count) == addrs.count else { return [] } + return addrs.map { $0.description } + } + + // true if the address is a previously generated internal or external address + func containsAddress(_ address: String) -> Bool { + return BRWalletContainsAddress(cPtr, address) != 0 + } + + func addressIsUsed(_ address: String) -> Bool { + return BRWalletAddressIsUsed(cPtr, address) != 0 + } + + // transactions registered in the wallet, sorted by date, oldest first + var transactions: [BRTxRef?] { + var transactions = [BRTxRef?](repeating: nil, count: BRWalletTransactions(cPtr, nil, 0)) + guard BRWalletTransactions(cPtr, &transactions, transactions.count) == transactions.count else { return [] } + return transactions + } + + // current wallet balance, not including transactions known to be invalid + var balance: UInt64 { + return BRWalletBalance(cPtr) + } + + // total amount spent from the wallet (exluding change) + var totalSent: UInt64 { + return BRWalletTotalSent(cPtr) + } + + // fee-per-kb of transaction size to use when creating a transaction + var feePerKb: UInt64 { + get { return BRWalletFeePerKb(cPtr) } + set(value) { BRWalletSetFeePerKb(cPtr, value) } + } + + func feeForTx(amount: UInt64) -> UInt64 { + return BRWalletFeeForTxAmount(cPtr, amount) + } + + // returns an unsigned transaction that sends the specified amount from the wallet to the given address + func createTransaction(forAmount: UInt64, toAddress: String) -> BRTxRef? { + return BRWalletCreateTransaction(cPtr, forAmount, toAddress) + } + + // returns an unsigned transaction that satisifes the given transaction outputs + func createTxForOutputs(_ outputs: [BRTxOutput]) -> BRTxRef { + return BRWalletCreateTxForOutputs(cPtr, outputs, outputs.count) + } + + // signs any inputs in tx that can be signed using private keys from the wallet + // forkId is 0 for bitcoin, 0x40 for b-cash + // seed is the master private key (wallet seed) corresponding to the master public key given when wallet was created + // returns true if all inputs were signed, or false if there was an error or not all inputs were able to be signed + func signTransaction(_ tx: BRTxRef, forkId: Int = 0, seed: inout UInt512) -> Bool { + return BRWalletSignTransaction(cPtr, tx, Int32(forkId), &seed, MemoryLayout.stride) != 0 + } + + // true if no previous wallet transaction spends any of the given transaction's inputs, and no inputs are invalid + func transactionIsValid(_ tx: BRTxRef) -> Bool { + return BRWalletTransactionIsValid(cPtr, tx) != 0 + } + + // true if transaction cannot be immediately spent (i.e. if it or an input tx can be replaced-by-fee) + func transactionIsPending(_ tx: BRTxRef) -> Bool { + return BRWalletTransactionIsPending(cPtr, tx) != 0 + } + + // true if tx is considered 0-conf safe (valid and not pending, timestamp greater than 0, and no unverified inputs) + func transactionIsVerified(_ tx: BRTxRef) -> Bool { + return BRWalletTransactionIsVerified(cPtr, tx) != 0 + } + + // the amount received by the wallet from the transaction (total outputs to change and/or receive addresses) + func amountReceivedFromTx(_ tx: BRTxRef) -> UInt64 { + return BRWalletAmountReceivedFromTx(cPtr, tx) + } + + // the amount sent from the wallet by the trasaction (total wallet outputs consumed, change and fee included) + func amountSentByTx(_ tx: BRTxRef) -> UInt64 { + return BRWalletAmountSentByTx(cPtr, tx) + } + + // returns the fee for the given transaction if all its inputs are from wallet transactions + func feeForTx(_ tx: BRTxRef) -> UInt64? { + let fee = BRWalletFeeForTx(cPtr, tx) + return fee == UINT64_MAX ? nil : fee + } + + // historical wallet balance after the given transaction, or current balance if tx is not registered in wallet + func balanceAfterTx(_ tx: BRTxRef) -> UInt64 { + return BRWalletBalanceAfterTx(cPtr, tx) + } + + // fee that will be added for a transaction of the given size in bytes + func feeForTxSize(_ size: Int) -> UInt64 { + return BRWalletFeeForTxSize(cPtr, size) + } + + // outputs below this amount are uneconomical due to fees (TX_MIN_OUTPUT_AMOUNT is the absolute min output amount) + var minOutputAmount: UInt64 { + return BRWalletMinOutputAmount(cPtr) + } + + // maximum amount that can be sent from the wallet to a single address after fees + var maxOutputAmount: UInt64 { + return BRWalletMaxOutputAmount(cPtr) + } + + deinit { + BRWalletFree(cPtr) + } +} diff --git a/loafwallet/UnsafeMutablePointerExtension.swift b/loafwallet/UnsafeMutablePointerExtension.swift new file mode 100644 index 000000000..1bb39973c --- /dev/null +++ b/loafwallet/UnsafeMutablePointerExtension.swift @@ -0,0 +1,100 @@ +import BRCore +import Foundation + +extension UnsafeMutablePointer where Pointee == BRTransaction { + init?() { + self.init(BRTransactionNew()) + } + + // bytes must contain a serialized tx + init?(bytes: [UInt8]) { + self.init(BRTransactionParse(bytes, bytes.count)) + } + + var txHash: UInt256 { + return pointee.txHash + } + + var version: UInt32 { + return pointee.version + } + + var inputs: [BRTxInput] { + return [BRTxInput](UnsafeBufferPointer(start: pointee.inputs, count: pointee.inCount)) + } + + var outputs: [BRTxOutput] { + return [BRTxOutput](UnsafeBufferPointer(start: pointee.outputs, count: pointee.outCount)) + } + + var lockTime: UInt32 { + return pointee.lockTime + } + + var blockHeight: UInt32 { + get { return pointee.blockHeight } + set { pointee.blockHeight = newValue } + } + + var timestamp: TimeInterval { + get { return pointee.timestamp > UInt32(NSTimeIntervalSince1970) ? + TimeInterval(pointee.timestamp) - NSTimeIntervalSince1970 : 0 + } + set { pointee.timestamp = newValue > 0 ? UInt32(newValue + NSTimeIntervalSince1970) : 0 } + } + + // serialized transaction (blockHeight and timestamp are not serialized) + var bytes: [UInt8]? { + var bytes = [UInt8](repeating: 0, count: BRTransactionSerialize(self, nil, 0)) + guard BRTransactionSerialize(self, &bytes, bytes.count) == bytes.count else { return nil } + return bytes + } + + // adds an input to tx + func addInput(txHash: UInt256, index: UInt32, amount: UInt64, script: [UInt8], + signature: [UInt8]? = nil, sequence: UInt32 = TXIN_SEQUENCE) + { + BRTransactionAddInput(self, txHash, index, amount, script, script.count, signature, signature?.count ?? 0, sequence) + } + + // adds an output to tx + func addOutput(amount: UInt64, script: [UInt8]) { + BRTransactionAddOutput(self, amount, script, script.count) + } + + // shuffles order of tx outputs + func shuffleOutputs() { + BRTransactionShuffleOutputs(self) + } + + // size in bytes if signed, or estimated size assuming compact pubkey sigs + var size: Int { + return BRTransactionSize(self) + } + + // minimum transaction fee needed for tx to relay across the bitcoin network + var standardFee: UInt64 { + return BRTransactionStandardFee(self) + } + + // checks if all signatures exist, but does not verify them + var isSigned: Bool { + return BRTransactionIsSigned(self) != 0 + } + + // adds signatures to any inputs with NULL signatures that can be signed with any keys + // forkId is 0 for bitcoin, 0x40 for b-cash + // returns true if tx is signed + func sign(forkId: Int = 0, keys: inout [BRKey]) -> Bool { + return BRTransactionSign(self, Int32(forkId), &keys, keys.count) != 0 + } + + public var hashValue: Int { + return BRTransactionHash(self) + } + + public static func == (l: UnsafeMutablePointer, r: UnsafeMutablePointer) -> Bool + { + return BRTransactionEq(l, r) != 0 + } +}