diff --git a/FullyNoded.xcodeproj/project.pbxproj b/FullyNoded.xcodeproj/project.pbxproj index 8c1514d8..0b986645 100644 --- a/FullyNoded.xcodeproj/project.pbxproj +++ b/FullyNoded.xcodeproj/project.pbxproj @@ -55,6 +55,8 @@ 0A6B365B27B1AA0C00CD4F3A /* AddressInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6B365A27B1AA0B00CD4F3A /* AddressInfo.swift */; }; 0A6B365D27B2020F00CD4F3A /* JMUtxo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6B365C27B2020F00CD4F3A /* JMUtxo.swift */; }; 0A6B365F27BD94CB00CD4F3A /* JMTx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6B365E27BD94CA00CD4F3A /* JMTx.swift */; }; + 0A6E59832C3C6AB4007750F2 /* URKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0A6E59822C3C6AB4007750F2 /* URKit */; }; + 0A6E59862C3CFE00007750F2 /* Bbqr in Frameworks */ = {isa = PBXBuildFile; productRef = 0A6E59852C3CFE00007750F2 /* Bbqr */; }; 0A6EF69B294E30F300D30E93 /* StreamManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6EF69A294E30F300D30E93 /* StreamManager.swift */; }; 0A77138426AD9C5E005CC23D /* BlindPsbt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A77138326AD9C5E005CC23D /* BlindPsbt.swift */; }; 0A77138626AE5E85005CC23D /* WalletInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A77138526AE5E85005CC23D /* WalletInfo.swift */; }; @@ -62,7 +64,6 @@ 0A8850FF2A4201C10078A603 /* LibWally.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD438842A41F7F90004F923 /* LibWally.xcframework */; }; 0A8851002A4201C10078A603 /* LibWally.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD438842A41F7F90004F923 /* LibWally.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0A9FDC9126A84CE30050C4AE /* WalletTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9FDC9026A84CE30050C4AE /* WalletTypes.swift */; }; - 0AB0B694290C374F00A1C201 /* URKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0AB0B693290C374F00A1C201 /* URKit */; }; 0AD8AD5029FF45B200F1ADD8 /* InvoiceSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD8AD4F29FF45B200F1ADD8 /* InvoiceSettingsViewController.swift */; }; 0AD9490A273C051B00AF83F5 /* ExternalFNWalletsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD94909273C051B00AF83F5 /* ExternalFNWalletsViewController.swift */; }; 0AE19ED725F4AC0D000F0AD4 /* WalletRecoveryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE19ED625F4AC0D000F0AD4 /* WalletRecoveryViewController.swift */; }; @@ -396,8 +397,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0AB0B694290C374F00A1C201 /* URKit in Frameworks */, + 0A6E59832C3C6AB4007750F2 /* URKit in Frameworks */, 0A8850FE2A41FB040078A603 /* secp256k1 in Frameworks */, + 0A6E59862C3CFE00007750F2 /* Bbqr in Frameworks */, D06D788C25B817B500769157 /* LifeHash in Frameworks */, 0AF73248291AF7D9009F6296 /* RNCryptor in Frameworks */, 0A8850FF2A4201C10078A603 /* LibWally.xcframework in Frameworks */, @@ -1017,9 +1019,10 @@ name = FullyNoded; packageProductDependencies = ( D06D788B25B817B500769157 /* LifeHash */, - 0AB0B693290C374F00A1C201 /* URKit */, 0AF73247291AF7D9009F6296 /* RNCryptor */, 0A8850FD2A41FB040078A603 /* secp256k1 */, + 0A6E59822C3C6AB4007750F2 /* URKit */, + 0A6E59852C3CFE00007750F2 /* Bbqr */, ); productName = BitSense; productReference = D00B9A242111427200E8B95A /* FullyNoded.app */; @@ -1093,9 +1096,10 @@ mainGroup = D00B9A1B2111427200E8B95A; packageReferences = ( D06D788A25B817B500769157 /* XCRemoteSwiftPackageReference "LifeHash" */, - 0AB0B692290C374F00A1C201 /* XCRemoteSwiftPackageReference "URKit" */, 0AF73246291AF7D9009F6296 /* XCRemoteSwiftPackageReference "RNCryptor" */, 0A8850FC2A41FB030078A603 /* XCRemoteSwiftPackageReference "secp256k1.swift" */, + 0A6E59812C3C6AB4007750F2 /* XCRemoteSwiftPackageReference "URKit" */, + 0A6E59842C3CFE00007750F2 /* XCRemoteSwiftPackageReference "bbqr-swift" */, ); productRefGroup = D00B9A252111427200E8B95A /* Products */; projectDirPath = ""; @@ -1534,12 +1538,12 @@ INFOPLIST_KEY_CFBundleDisplayName = "Fully Noded"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_PREPROCESS = NO; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.484; + MARKETING_VERSION = 1.487; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.fontaine.FullyNoded; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = com.fontaine.fullynodedmacos; @@ -1581,12 +1585,12 @@ INFOPLIST_KEY_CFBundleDisplayName = "Fully Noded"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_PREPROCESS = NO; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.484; + MARKETING_VERSION = 1.487; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.fontaine.FullyNoded; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = com.fontaine.fullynodedmacos; @@ -1684,19 +1688,27 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 0A8850FC2A41FB030078A603 /* XCRemoteSwiftPackageReference "secp256k1.swift" */ = { + 0A6E59812C3C6AB4007750F2 /* XCRemoteSwiftPackageReference "URKit" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/jb55/secp256k1.swift.git"; + repositoryURL = "https://github.com/BlockchainCommons/URKit.git"; requirement = { - branch = main; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 14.0.2; }; }; - 0AB0B692290C374F00A1C201 /* XCRemoteSwiftPackageReference "URKit" */ = { + 0A6E59842C3CFE00007750F2 /* XCRemoteSwiftPackageReference "bbqr-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/bitcoinppl/bbqr-swift.git"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 0.3.1; + }; + }; + 0A8850FC2A41FB030078A603 /* XCRemoteSwiftPackageReference "secp256k1.swift" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Fonta1n3/URKit"; + repositoryURL = "https://github.com/jb55/secp256k1.swift.git"; requirement = { - branch = master; + branch = main; kind = branch; }; }; @@ -1719,16 +1731,21 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 0A6E59822C3C6AB4007750F2 /* URKit */ = { + isa = XCSwiftPackageProductDependency; + package = 0A6E59812C3C6AB4007750F2 /* XCRemoteSwiftPackageReference "URKit" */; + productName = URKit; + }; + 0A6E59852C3CFE00007750F2 /* Bbqr */ = { + isa = XCSwiftPackageProductDependency; + package = 0A6E59842C3CFE00007750F2 /* XCRemoteSwiftPackageReference "bbqr-swift" */; + productName = Bbqr; + }; 0A8850FD2A41FB040078A603 /* secp256k1 */ = { isa = XCSwiftPackageProductDependency; package = 0A8850FC2A41FB030078A603 /* XCRemoteSwiftPackageReference "secp256k1.swift" */; productName = secp256k1; }; - 0AB0B693290C374F00A1C201 /* URKit */ = { - isa = XCSwiftPackageProductDependency; - package = 0AB0B692290C374F00A1C201 /* XCRemoteSwiftPackageReference "URKit" */; - productName = URKit; - }; 0AF73247291AF7D9009F6296 /* RNCryptor */ = { isa = XCSwiftPackageProductDependency; package = 0AF73246291AF7D9009F6296 /* XCRemoteSwiftPackageReference "RNCryptor" */; diff --git a/FullyNoded.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FullyNoded.xcworkspace/xcshareddata/swiftpm/Package.resolved index 371ec6a5..2f8b42a4 100644 --- a/FullyNoded.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FullyNoded.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "80d738e01d019027b7e2198930ba90237b181310c1fceceb2bbaca0d1aae4117", + "originHash" : "dbc57cb905ea6d2c86cdcb51ff9b9cf19432f8eab192b534539f6a1f065226c0", "pins" : [ + { + "identity" : "bbqr-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/bitcoinppl/bbqr-swift.git", + "state" : { + "revision" : "83b828077ecc4f5d2cf8889da5543a61b4a60a3c", + "version" : "0.3.1" + } + }, { "identity" : "bc-lifehash", "kind" : "remoteSourceControl", @@ -10,6 +19,33 @@ "version" : "0.4.1" } }, + { + "identity" : "bcswiftdcbor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/BlockchainCommons/BCSwiftDCBOR", + "state" : { + "revision" : "922746297fe733019b42b9083275eb2837296a7d", + "version" : "1.0.5" + } + }, + { + "identity" : "bcswiftfloat16", + "kind" : "remoteSourceControl", + "location" : "https://github.com/blockchaincommons/BCSwiftFloat16", + "state" : { + "revision" : "71f380285d88a876f9cdf4e34e2e9e77d7ca4aef", + "version" : "1.0.0" + } + }, + { + "identity" : "bcswifttags", + "kind" : "remoteSourceControl", + "location" : "https://github.com/BlockchainCommons/BCSwiftTags", + "state" : { + "revision" : "edb94b337d03e01c871db895c65d8fd3b6d43c9c", + "version" : "0.1.2" + } + }, { "identity" : "lifehash", "kind" : "remoteSourceControl", @@ -37,13 +73,31 @@ "revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9" } }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/wolfmcnally/swift-collections", + "state" : { + "revision" : "53a8adc54374f620002a3b6401d39e0feb3c57ae", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-numberkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/wolfmcnally/swift-numberkit.git", + "state" : { + "revision" : "34a26297c200489779929b7fd7d4d30b63e87e69", + "version" : "2.4.3" + } + }, { "identity" : "urkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/Fonta1n3/URKit", + "location" : "https://github.com/BlockchainCommons/URKit.git", "state" : { - "branch" : "master", - "revision" : "1d74d3e758130ac7e20f9b66221866928c81ef96" + "revision" : "c98261c7b2db42ccfbbba0853379826b86aaffc4", + "version" : "14.0.2" } } ], diff --git a/FullyNoded/Base.lproj/Main.storyboard b/FullyNoded/Base.lproj/Main.storyboard index 000dc176..475f6a76 100644 --- a/FullyNoded/Base.lproj/Main.storyboard +++ b/FullyNoded/Base.lproj/Main.storyboard @@ -1907,7 +1907,7 @@ xxxxx - + @@ -1934,13 +1934,23 @@ xxxxx + + - + @@ -1948,11 +1958,13 @@ xxxxx + + @@ -2607,43 +2619,131 @@ xxxxx - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - - - - + + + + + - + @@ -2679,7 +2779,7 @@ xxxxx - + @@ -2713,20 +2813,20 @@ xxxxx - + - + - - - - - - - - - - - - - - - - - - @@ -3190,8 +3258,8 @@ Fully Noded does its best to support as many hardware and software wallet option - - + + @@ -3211,44 +3279,79 @@ Fully Noded does its best to support as many hardware and software wallet option - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3277,6 +3391,7 @@ Fully Noded does its best to support as many hardware and software wallet option + @@ -4203,6 +4318,12 @@ Fully Noded does its best to support as many hardware and software wallet option + @@ -4211,9 +4332,11 @@ Fully Noded does its best to support as many hardware and software wallet option + + @@ -4239,6 +4362,7 @@ Fully Noded does its best to support as many hardware and software wallet option + @@ -4329,7 +4453,7 @@ You can upload the wallets.fullynoded file that was created when you made the ba - + @@ -4570,7 +4694,7 @@ You can upload the wallets.fullynoded file that was created when you made the ba - + @@ -6942,8 +7066,8 @@ You can upload the wallets.fullynoded file that was created when you made the ba - - + + diff --git a/FullyNoded/Extensions.swift b/FullyNoded/Extensions.swift index e77cf1a5..b84aa4d6 100644 --- a/FullyNoded/Extensions.swift +++ b/FullyNoded/Extensions.swift @@ -282,7 +282,7 @@ public extension Data { public extension Double { func rounded(toPlaces places:Int) -> Double { - let divisor = pow(10.0, Double(places)) + let divisor = Darwin.pow(10.0, Double(places)) return (self * divisor).rounded() / divisor } @@ -406,6 +406,45 @@ public extension Double { var satsToBtcDouble: Double { return self / 100000000.0 } + + var btcBalanceWithSpaces: String { + var btcBalance = Swift.abs(self.rounded(toPlaces: 8)).avoidNotation + if !btcBalance.contains(".") { + btcBalance += ".0" + } + + if self == 0.0 { + btcBalance = "0.00 000 000" + } else { + var decimalLocation = 0 + var btcBalanceArray:[String] = [] + var digitsPastDecimal = 0 + + for (i, c) in btcBalance.enumerated() { + btcBalanceArray.append("\(c)") + if c == "." { + decimalLocation = i + } + if i > decimalLocation { + digitsPastDecimal += 1 + } + } + + if digitsPastDecimal <= 7 { + let numberOfTrailingZerosNeeded = 7 - digitsPastDecimal + + for _ in 0...numberOfTrailingZerosNeeded { + btcBalanceArray.append("0") + } + } + + btcBalanceArray.insert(" ", at: decimalLocation + 3) + btcBalanceArray.insert(" ", at: decimalLocation + 7) + btcBalance = btcBalanceArray.joined() + } + + return btcBalance + } } public extension Int { diff --git a/FullyNoded/Helpers/AccountMap.swift b/FullyNoded/Helpers/AccountMap.swift index 1d8ef367..d82c463b 100644 --- a/FullyNoded/Helpers/AccountMap.swift +++ b/FullyNoded/Helpers/AccountMap.swift @@ -27,24 +27,24 @@ class AccountMap { let ds = Descriptor(primDesc) if ds.isHot && !ds.isMulti { - if let key = try? HDKey(base58: ds.accountXprv) { - primDesc = primDesc.replacingOccurrences(of: ds.accountXprv, with: key.xpub) - - for (i, _) in watching.enumerated() { - watching[i] = watching[i].replacingOccurrences(of: ds.accountXprv, with: key.xpub) - } - } +// if let key = try? HDKey(base58: ds.accountXprv) { +// primDesc = primDesc.replacingOccurrences(of: ds.accountXprv, with: key.xpub) +// +// for (i, _) in watching.enumerated() { +// watching[i] = watching[i].replacingOccurrences(of: ds.accountXprv, with: key.xpub) +// } +// } } else if ds.isHot { for key in ds.multiSigKeys { - if key.hasPrefix("xprv") || key.hasPrefix("tprv") { - if let hdkey = try? HDKey(base58: key) { - primDesc = primDesc.replacingOccurrences(of: key, with: hdkey.xpub) - - for (i, _) in watching.enumerated() { - watching[i] = watching[i].replacingOccurrences(of: key, with: hdkey.xpub) - } - } - } +// if key.hasPrefix("xprv") || key.hasPrefix("tprv") { +// if let hdkey = try? HDKey(base58: key) { +// primDesc = primDesc.replacingOccurrences(of: key, with: hdkey.xpub) +// +// for (i, _) in watching.enumerated() { +// watching[i] = watching[i].replacingOccurrences(of: key, with: hdkey.xpub) +// } +// } +// } } } diff --git a/FullyNoded/Helpers/Tor/TorClient.swift b/FullyNoded/Helpers/Tor/TorClient.swift index 65d072d7..4445838f 100644 --- a/FullyNoded/Helpers/Tor/TorClient.swift +++ b/FullyNoded/Helpers/Tor/TorClient.swift @@ -50,10 +50,10 @@ class TorClient: NSObject, URLSessionDelegate { state = .started var proxyPort = 19050 - var dnsPort = 12345 + //var dnsPort = 12345 #if targetEnvironment(simulator) proxyPort = 19052 - dnsPort = 12347 + //dnsPort = 12347 #endif sessionConfiguration.connectionProxyDictionary = [kCFProxyTypeKey: kCFProxyTypeSOCKS, @@ -81,7 +81,7 @@ class TorClient: NSObject, URLSessionDelegate { self.thread = nil self.config.options = [ - "DNSPort": "\(dnsPort)", + //"DNSPort": "\(dnsPort)", "AutomapHostsOnResolve": "1", "SocksPort": "\(proxyPort)",//OnionTrafficOnly "AvoidDiskWrites": "1", diff --git a/FullyNoded/Helpers/UR/Asset.swift b/FullyNoded/Helpers/UR/Asset.swift index e438c792..693d77d9 100644 --- a/FullyNoded/Helpers/UR/Asset.swift +++ b/FullyNoded/Helpers/UR/Asset.swift @@ -15,12 +15,12 @@ enum Asset: UInt32, Identifiable, CaseIterable { // case bch = 0x91 var cbor: CBOR { - CBOR.unsignedInt(UInt64(rawValue)) + CBOR.unsigned(UInt64(rawValue)) } init(cbor: CBOR) throws { guard - case let CBOR.unsignedInt(r) = cbor, + case let CBOR.unsigned(r) = cbor, let a = Asset(rawValue: UInt32(r)) else { throw GeneralError("Invalid Asset.") } diff --git a/FullyNoded/Helpers/UR/CBORExtensions.swift b/FullyNoded/Helpers/UR/CBORExtensions.swift index 162e1a06..51b98ad6 100644 --- a/FullyNoded/Helpers/UR/CBORExtensions.swift +++ b/FullyNoded/Helpers/UR/CBORExtensions.swift @@ -9,16 +9,15 @@ import Foundation import URKit -extension CBOR.Tag { - static let seed = CBOR.Tag(rawValue: 300) - static let hdKey = CBOR.Tag(rawValue: 303) - static let derivationPath = CBOR.Tag(rawValue: 304) - static let useInfo = CBOR.Tag(rawValue: 305) - static let sskrShare = CBOR.Tag(rawValue: 309) - static let transactionRequest = CBOR.Tag(rawValue: 312) - static let transactionResponse = CBOR.Tag(rawValue: 313) - - static let seedRequestBody = CBOR.Tag(rawValue: 500) - static let keyRequestBody = CBOR.Tag(rawValue: 501) - static let psbtSignatureRequestBody = CBOR.Tag(rawValue: 502) +extension Tag { + static let seed = Tag(300) + static let hdKey = Tag(303) + static let derivationPath = Tag(304) + static let useInfo = Tag(305) + static let sskrShare = Tag(309) + static let transactionRequest = Tag(312) + static let transactionResponse = Tag(313) + static let seedRequestBody = Tag(500) + static let keyRequestBody = Tag(501) + static let psbtSignatureRequestBody = Tag(502) } diff --git a/FullyNoded/Helpers/UR/ChildIndex.swift b/FullyNoded/Helpers/UR/ChildIndex.swift index 270ecd64..c0f5bcfd 100644 --- a/FullyNoded/Helpers/UR/ChildIndex.swift +++ b/FullyNoded/Helpers/UR/ChildIndex.swift @@ -31,11 +31,11 @@ struct ChildIndex: ExpressibleByIntegerLiteral { } var cbor: CBOR { - CBOR.unsignedInt(UInt64(value)) + CBOR.unsigned(UInt64(value)) } init?(cbor: CBOR) throws { - guard case let CBOR.unsignedInt(value) = cbor else { + guard case let CBOR.unsigned(value) = cbor else { return nil } guard value < 0x80000000 else { diff --git a/FullyNoded/Helpers/UR/ChildIndexRange.swift b/FullyNoded/Helpers/UR/ChildIndexRange.swift index 9682689a..2e206af8 100644 --- a/FullyNoded/Helpers/UR/ChildIndexRange.swift +++ b/FullyNoded/Helpers/UR/ChildIndexRange.swift @@ -22,8 +22,8 @@ struct ChildIndexRange { var cbor: CBOR { CBOR.array([ - CBOR.unsignedInt(UInt64(low.value)), - CBOR.unsignedInt(UInt64(high.value)) + CBOR.unsigned(UInt64(low.value)), + CBOR.unsigned(UInt64(high.value)) ]) } @@ -35,8 +35,8 @@ struct ChildIndexRange { return nil } guard - case let CBOR.unsignedInt(low) = array[0], - case let CBOR.unsignedInt(high) = array[1] + case let CBOR.unsigned(low) = array[0], + case let CBOR.unsigned(high) = array[1] else { return nil } diff --git a/FullyNoded/Helpers/UR/DerivationPath.swift b/FullyNoded/Helpers/UR/DerivationPath.swift index 2fd45c8a..d0111b8b 100644 --- a/FullyNoded/Helpers/UR/DerivationPath.swift +++ b/FullyNoded/Helpers/UR/DerivationPath.swift @@ -37,19 +37,22 @@ struct DerivationPath: ExpressibleByArrayLiteral { } var cbor: CBOR { - var a: [OrderedMapEntry] = [ - .init(key: 1, value: CBOR.array(steps.flatMap { $0.array } )) + var a: Map = [ + CBOR.unsigned(1): CBOR.array(steps.flatMap { $0.array })//.init(key: 1, value: CBOR.array(steps.flatMap { $0.array } )) + ] if let sourceFingerprint = sourceFingerprint { - a.append(.init(key: 2, value: CBOR.unsignedInt(UInt64(sourceFingerprint)))) + //a.append(.init(key: 2, value: CBOR.unsignedInt(UInt64(sourceFingerprint)))) + a.insert(CBOR.unsigned(2), CBOR.unsigned(UInt64(sourceFingerprint))) } if let depth = depth { - a.append(.init(key: 3, value: CBOR.unsignedInt(UInt64(depth)))) + //a.append(.init(key: 3, value: CBOR.unsignedInt(UInt64(depth)))) + a.insert(CBOR.unsigned(3), CBOR.unsigned(UInt64(depth))) } - return CBOR.orderedMap(a) + return CBOR.map(a) } var taggedCBOR: CBOR { @@ -73,16 +76,22 @@ struct DerivationPath: ExpressibleByArrayLiteral { let steps: [DerivationStep] = try stride(from: 0, to: componentsItem.count, by: 2).map { i in let childIndexSpec = try ChildIndexSpec.decode(cbor: componentsItem[i]) - guard case let CBOR.boolean(isHardened) = componentsItem[i + 1] else { + guard let isHardened = try? BooleanLiteralType(cbor: componentsItem[i + 1]) else { print("Invalid path component.") throw GeneralError("Invalid path component.") } return DerivationStep(childIndexSpec, isHardened: isHardened) +// guard case let CBOR.booleanLiteral(isHardened) = componentsItem[i + 1] else { +// print("Invalid path component.") +// throw GeneralError("Invalid path component.") +// } +// return DerivationStep(childIndexSpec, isHardened: isHardened) } let sourceFingerprint: UInt32? - if let sourceFingerprintItem = pairs[2] { - if case let CBOR.unsignedInt(sourceFingerprintValue) = sourceFingerprintItem, sourceFingerprintValue != 0, sourceFingerprintValue <= UInt32.max { + + if let sourceFingerprintItem = pairs.get(2) { + if case let CBOR.unsigned(sourceFingerprintValue) = sourceFingerprintItem, sourceFingerprintValue != 0, sourceFingerprintValue <= UInt32.max { sourceFingerprint = UInt32(sourceFingerprintValue) } else { sourceFingerprint = nil @@ -93,9 +102,10 @@ struct DerivationPath: ExpressibleByArrayLiteral { } let depth: UInt8? - if let depthItem = pairs[3] { + + if let depthItem = pairs.get(3) { guard - case let CBOR.unsignedInt(depthValue) = depthItem, + case let CBOR.unsigned(depthValue) = depthItem, depthValue <= UInt8.max else { print("Invalid depth.") diff --git a/FullyNoded/Helpers/UR/DerivationStep.swift b/FullyNoded/Helpers/UR/DerivationStep.swift index 3d761224..d43f588e 100644 --- a/FullyNoded/Helpers/UR/DerivationStep.swift +++ b/FullyNoded/Helpers/UR/DerivationStep.swift @@ -23,7 +23,7 @@ struct DerivationStep { } var array: [CBOR] { - [childIndexSpec.cbor, CBOR.boolean(isHardened)] + [childIndexSpec.cbor, CBOR(booleanLiteral: isHardened)] } func childNum() throws -> UInt32 { diff --git a/FullyNoded/Helpers/UR/HDKey.swift b/FullyNoded/Helpers/UR/HDKey.swift index 886a81e6..0b276e40 100644 --- a/FullyNoded/Helpers/UR/HDKey.swift +++ b/FullyNoded/Helpers/UR/HDKey.swift @@ -302,41 +302,50 @@ extension HDKey_: Equatable { extension HDKey_ { var cbor: CBOR { - var a: [OrderedMapEntry] = [] + var a: Map = [:] if isMaster { - a.append(.init(key: 1, value: true)) + //a.append(.init(key: 1, value: true)) + a.insert(CBOR.unsigned(1), CBOR(booleanLiteral: true)) } if keyType == .private { - a.append(.init(key: 2, value: true)) + //a.append(.init(key: 2, value: true)) + a.insert(CBOR.unsigned(2), CBOR(booleanLiteral: true)) } - a.append(.init(key: 3, value: CBOR.byteString(keyData.bytes))) + //a.append(.init(key: 3, value: CBOR.byteString(keyData.bytes))) + a.insert(CBOR.unsigned(3), CBOR.bytes(keyData.cborData)) if let chainCode = chainCode { - a.append(.init(key: 4, value: CBOR.byteString(chainCode.bytes))) + //a.append(.init(key: 4, value: CBOR.byteString(chainCode.bytes))) + a.insert(CBOR.unsigned(4), CBOR.bytes(chainCode.cborData)) } if let useinfo = useInfo { if !useinfo.isDefault { - a.append(.init(key: 5, value: useinfo.taggedCBOR)) + //a.append(.init(key: 5, value: useinfo.taggedCBOR)) + a.insert(CBOR.unsigned(5), useinfo.taggedCBOR) } } if let origin = origin { - a.append(.init(key: 6, value: origin.taggedCBOR)) + //a.append(.init(key: 6, value: origin.taggedCBOR)) + a.insert(CBOR.unsigned(6), origin.taggedCBOR) } if let children = children { - a.append(.init(key: 7, value: children.taggedCBOR)) + //a.append(.init(key: 7, value: children.taggedCBOR)) + a.insert(CBOR.unsigned(7), children.taggedCBOR) } if let parentFingerprint = parentFingerprint { - a.append(.init(key: 8, value: CBOR.unsignedInt(UInt64(parentFingerprint)))) + //a.append(.init(key: 8, value: CBOR.unsignedInt(UInt64(parentFingerprint)))) + a.insert(CBOR.unsigned(8), CBOR.unsigned(UInt64(parentFingerprint))) } - return CBOR.orderedMap(a) + //return CBOR.orderedMap(a) + return CBOR.map(a) } var taggedCBOR: CBOR { @@ -357,22 +366,32 @@ extension HDKey_ { throw GeneralError("HDKey: Doesn't contain a map.") } - guard case let CBOR.boolean(isMaster) = pairs[1] ?? CBOR.boolean(false) else { + guard let isMaster = try? BooleanLiteralType(cbor: pairs[1] ?? CBOR(booleanLiteral: false)) else { print("HDKey: Invalid `isMaster` field.") throw GeneralError("HDKey: Invalid `isMaster` field.") } - guard case let CBOR.boolean(isPrivate) = pairs[2] ?? CBOR.boolean(isMaster) else { +// guard case let CBOR.boolean(isMaster) = pairs[1] ?? CBOR.boolean(false) else { +// print("HDKey: Invalid `isMaster` field.") +// throw GeneralError("HDKey: Invalid `isMaster` field.") +// } + + guard let isPrivate = try? BooleanLiteralType(cbor: pairs[2] ?? CBOR(booleanLiteral: isMaster)) else { print("HDKey: Invalid `isPrivate` field.") throw GeneralError("HDKey: Invalid `isPrivate` field.") } +// guard case let CBOR.boolean(isPrivate) = pairs[2] ?? CBOR.boolean(isMaster) else { +// print("HDKey: Invalid `isPrivate` field.") +// throw GeneralError("HDKey: Invalid `isPrivate` field.") +// } + if isMaster && !isPrivate { print("HDKey: Master key cannot be public.") throw GeneralError("HDKey: Master key cannot be public.") } - guard case let CBOR.byteString(keyDataValue) = pairs[3] ?? CBOR.null, + guard case let CBOR.bytes(keyDataValue) = pairs[3] ?? CBOR.null, keyDataValue.count == 33 else { print("HDKey: Invalid key data.") throw GeneralError("HDKey: Invalid key data.") @@ -381,8 +400,8 @@ extension HDKey_ { let keyData = Data(keyDataValue) let chainCode: Data? - if let chainCodeItem = pairs[4] { - guard case let CBOR.byteString(chainCodeValue) = chainCodeItem, + if let chainCodeItem = pairs.get(4) { + guard case let CBOR.bytes(chainCodeValue) = chainCodeItem, chainCodeValue.count == 32 else { print("HDKey: Invalid key chain code.") throw GeneralError("HDKey: Invalid key chain code.") @@ -404,23 +423,23 @@ extension HDKey_ { let origin: DerivationPath? - if let originItem = pairs[6] { + if let originItem = pairs.get(6) { origin = try DerivationPath(taggedCBOR: originItem) } else { origin = nil } let children: DerivationPath? - if let childrenItem = pairs[7] { + if let childrenItem = pairs.get(7) { children = try DerivationPath(taggedCBOR: childrenItem) } else { children = nil } let parentFingerprint: UInt32? - if let parentFingerprintItem = pairs[8] { + if let parentFingerprintItem = pairs.get(8) { guard - case let CBOR.unsignedInt(parentFingerprintValue) = parentFingerprintItem, + case let CBOR.unsigned(parentFingerprintValue) = parentFingerprintItem, parentFingerprintValue > 0, parentFingerprintValue <= UInt32.max else { @@ -439,10 +458,17 @@ extension HDKey_ { convenience init(taggedCBOR: CBOR) throws { print("convenience init(taggedCBOR: CBOR)") + +// guard case let CBOR.tagged(.hdKeyV1, cbor) = taggedCBOR else { +// print("HDKeyV1 tag (303) not found.") +// throw GeneralError("HDKeyV1 tag (303) not found.") +// } + guard case let CBOR.tagged(.hdKey, cbor) = taggedCBOR else { print("HDKey tag (303) not found.") throw GeneralError("HDKey tag (303) not found.") } + try self.init(cbor: cbor) } } @@ -451,20 +477,20 @@ extension HDKey_: Fingerprintable { var fingerprintData: Data { var result: [CBOR] = [] - result.append(CBOR.byteString(keyData.bytes)) + result.append(CBOR.bytes(keyData.cborData)) if let chainCode = chainCode { - result.append(CBOR.byteString(chainCode.bytes)) + result.append(CBOR.bytes(chainCode.cborData)) } else { result.append(CBOR.null) } if let useinfo = useInfo { - result.append(CBOR.unsignedInt(UInt64(useinfo.asset.rawValue))) - result.append(CBOR.unsignedInt(UInt64(useinfo.network.rawValue))) + result.append(CBOR.unsigned(UInt64(useinfo.asset.rawValue))) + result.append(CBOR.unsigned(UInt64(useinfo.network.rawValue))) } - return Data(result.encode()) + return result.cborData } } diff --git a/FullyNoded/Helpers/UR/Network.swift b/FullyNoded/Helpers/UR/Network.swift index e1abf3ae..b15aff44 100644 --- a/FullyNoded/Helpers/UR/Network.swift +++ b/FullyNoded/Helpers/UR/Network.swift @@ -14,12 +14,12 @@ enum Network_: UInt32, Identifiable, CaseIterable { case testnet = 1 var cbor: CBOR { - CBOR.unsignedInt(UInt64(rawValue)) + CBOR.unsigned(UInt64(rawValue)) } init(cbor: CBOR) throws { guard - case let CBOR.unsignedInt(r) = cbor, + case let CBOR.unsigned(r) = cbor, let a = Network_(rawValue: UInt32(r)) else { throw GeneralError("Invalid Network.") } diff --git a/FullyNoded/Helpers/UR/UR.swift b/FullyNoded/Helpers/UR/UR.swift index 27d4bc4f..56ddd354 100644 --- a/FullyNoded/Helpers/UR/UR.swift +++ b/FullyNoded/Helpers/UR/UR.swift @@ -11,7 +11,7 @@ import URKit class URHelper { - enum scriptTag:CBOR.Tag { + enum scriptTag: Tag { case wpkh = 404 case wsh = 401 case sh = 400 @@ -26,12 +26,14 @@ class URHelper { } static func bytesToData(_ ur: UR) -> Data? { - guard let decodedCbor = try? CBOR.decode(ur.cbor.bytes), - case let CBOR.byteString(bytes) = decodedCbor else { - return nil - } - - return Data(bytes) + //ur.cbor.cborData +// guard let decodedCbor = try? ur.cbor.cborData.bytes.data, +// case let CBOR.bytes(bytes) = decodedCbor else { +// return nil +// } +// +// return Data(bytes) + return ur.cbor.cborData.bytes.data } static func ur(_ string: String) -> UR? { @@ -39,24 +41,25 @@ class URHelper { } static func dataToUrBytes(_ data: Data) -> UR? { - let cbor = CBOR.byteString(data.bytes).cborEncode().data - return try? UR(type: "bytes", cbor: cbor) + return try? UR(type: "bytes", untaggedCBOR: data) } static func psbtUr(_ data: Data) -> UR? { - let cbor = CBOR.encodeByteString(data.bytes).data + //let cbor = CBOR.encodeByteString(data.bytes).data - return try? UR(type: "crypto-psbt", cbor: cbor) + return try? UR(type: "crypto-psbt", cbor: data.cbor) } static func psbtUrToBase64Text(_ ur: UR) -> String? { - guard let decodedCbor = try? CBOR.decode(ur.cbor.bytes), - case let CBOR.byteString(bytes) = decodedCbor else { - return nil - } +// guard let decodedCbor = try? CBOR.decode(ur.cbor.bytes), +// case let CBOR.byteString(bytes) = decodedCbor else { +// return nil +// } - return Data(bytes).base64EncodedString() + return ur.cbor.cborData.base64EncodedString() + + //return Data(bytes).base64EncodedString() } static func parseUr(urString: String) -> (descriptors: [String]?, error: String?) { @@ -97,31 +100,45 @@ class URHelper { static func entropyToUr(data: Data) -> String? { let wrapper:CBOR = .map([ - .unsignedInt(1) : .byteString(data.bytes), + CBOR.unsigned(1) : data.cborData ]) - let cbor = Data(wrapper.cborEncode()) - do { - let rawUr = try UR(type: "crypto-seed", cbor: cbor) - return UREncoder.encode(rawUr) - } catch { - return nil - } + + let cbor = wrapper.cbor + + guard let rawUr = try? UR(type: "crypto-seed", cbor: cbor) else { return nil } + + return UREncoder.encode(rawUr) + +// let wrapper:CBOR = .map([ +// .unsignedInt(1) : .byteString(data.bytes) +// ]) +// let cbor = Data(wrapper.cborEncode()) +// do { +// let rawUr = try UR(type: "crypto-seed", cbor: cbor) +// return UREncoder.encode(rawUr) +// } catch { +// return nil +// } } static func urToEntropy(urString: String) -> (data: Data?, birthdate: UInt64?) { do { let ur = try URDecoder.decode(urString) - let decodedCbor = try CBOR.decode(ur.cbor.bytes) - guard case let CBOR.map(dict) = decodedCbor! else { return (nil, nil) } + let decodedCbor = ur.cbor//try CBOR.decode(ur.cbor.bytes) + guard case let CBOR.map(dict) = decodedCbor else { return (nil, nil) } + var data:Data? var birthdate:UInt64? + for (key, value) in dict { switch key { case 1: - guard case let CBOR.byteString(byteString) = value else { fallthrough } - data = Data(byteString) + guard case let CBOR.bytes(byteString) = value else { fallthrough } + + data = byteString case 2: - guard case let CBOR.unsignedInt(n) = value else { fallthrough } + guard case let CBOR.unsigned(n) = value else { fallthrough } + birthdate = n default: break @@ -134,23 +151,32 @@ class URHelper { } static func parseBlueWalletCoordinationSetup(_ urString: String) -> (text: String?, error: String?) { - guard let ur = ur(urString), let decodedCbor = try? CBOR.decode(ur.cbor.bytes), - case let CBOR.byteString(bytes) = decodedCbor, - let text = Data(bytes).utf8String else { - return (nil, "Unable to decode the QR code into a text file.") - } + guard let ur = try? UR(urString: urString) else { return ((nil, "Unable to convert string to UR."))} + + guard let text = ur.cbor.cborData.utf8String else { return ((nil, "Unable to convert ur to text."))} + +// guard let decodedCbor = try? CBOR.decode(ur.cbor.bytes), +// case let CBOR.byteString(bytes) = decodedCbor, +// let text = Data(bytes).utf8String else { +// return (nil, "Unable to decode the QR code into a text file.") +// } return (text, nil) } static func parseCryptoOutput(_ urString: String) -> (descriptors: [String]?, error: String?) { - guard let ur = try? URDecoder.decode(urString.condenseWhitespace()), - let decodedCbor = try? CBOR.decode(ur.cbor.bytes), - case let CBOR.tagged(tag, taggedCbor) = decodedCbor else { - return (nil, "Error decoding your output UR.") - } + guard let ur = try? UR(urString: urString) else { return ((nil, "Unable to convert string to UR."))} + + + guard case let CBOR.tagged(tag, taggedCbor) = ur.cbor else { return((nil, "Unable to convert ")) } - switch tag.rawValue { +// guard let ur = try? URDecoder.decode(urString.condenseWhitespace()), +// let decodedCbor = try? CBOR.decode(ur.cbor.bytes), +// case let CBOR.tagged(tag, taggedCbor) = decodedCbor else { +// return (nil, "Error decoding your output UR.") +// } + + switch tag { case 400: // script-hash return parseSHCbor(taggedCbor: taggedCbor) @@ -186,6 +212,8 @@ class URHelper { } static func parseWPKHCbor(taggedCbor: CBOR) -> (descriptors: [String]?, error: String?) { + print("taggedCbor: \(taggedCbor)") + guard let key = try? HDKey_(taggedCBOR: taggedCbor), let origin = key.origin, let extKey = key.base58 else { @@ -334,7 +362,7 @@ class URHelper { for (key, value) in map { switch key { case 1: - guard case let CBOR.unsignedInt(thresholdRaw) = value else { + guard case let CBOR.unsigned(thresholdRaw) = value else { return (nil, "Invalid multisig hdkey, no threshold provided.") } @@ -397,9 +425,9 @@ class URHelper { } static func parseCryptoAccount(_ urString: String) -> (descriptors: [String]?, error: String?) { - guard let ur = try? URDecoder.decode(urString.condenseWhitespace()), - let decodedCbor = try? CBOR.decode(ur.cbor.bytes), - case let CBOR.map(dict) = decodedCbor else { + guard let ur = try? UR(urString: urString) else { return ((nil, "Unable to convert string to UR."))} + + guard case let CBOR.map(dict) = ur.cbor else { return (nil, "Error decoding account UR.") } @@ -413,7 +441,7 @@ class URHelper { switch key { case 1: - guard case let CBOR.unsignedInt(fingerprint) = value else { + guard case let CBOR.unsigned(fingerprint) = value else { error = "Unable to decode the master key fingerprint." fallthrough } @@ -425,7 +453,7 @@ class URHelper { for (i, elem) in accounts.enumerated() { if case let CBOR.tagged(tag, taggedCbor) = elem { - switch tag.rawValue { + switch tag { case 400: // script-hash @@ -513,12 +541,17 @@ class URHelper { static func parseHdkey(urString: String) -> (descriptors: [String]?, error: String?) { var descriptor:[String]? - guard let ur = try? URDecoder.decode(urString.condenseWhitespace()), - let decodedCbor = try? CBOR.decode(ur.cbor.bytes), - let hdkey = try? HDKey_.init(cbor: decodedCbor), - let origin = hdkey.origin else { - return (nil, "UR decoding/hdkey/conversion/origin missing.") - } + guard let ur = try? UR(urString: urString) else { return ((nil, "Unable to convert string to UR."))} + let decodedCbor = ur.cbor + guard let hdkey = try? HDKey_.init(cbor: decodedCbor) else { return ((nil, "Unable to init hdkey from deocedCbor"))} + guard let origin = hdkey.origin else { return ((nil, "Unable to get origin from hdkey."))} + +// guard let ur = try? URDecoder.decode(urString.condenseWhitespace()), +// let decodedCbor = try? CBOR.decode(ur.cbor.bytes), +// let hdkey = try? HDKey_.init(cbor: decodedCbor), +// let origin = hdkey.origin else { +// return (nil, "UR decoding/hdkey/conversion/origin missing.") +// } let path = origin.description @@ -652,34 +685,50 @@ class URHelper { return nil } - var originsArray:[OrderedMapEntry] = [] - originsArray.append(.init(key: 1, value: .array(origins(path: descriptor.derivation)))) - originsArray.append(.init(key: 2, value: .unsignedInt(UInt64(descriptor.fingerprint, radix: 16) ?? 0))) - originsArray.append(.init(key: 3, value: .unsignedInt(UInt64(depth.hexString) ?? 0))) - let originsWrapper = CBOR.orderedMap(originsArray) +// var originsArray:[OrderedMapEntry] = [] +// originsArray.append(.init(key: 1, value: .array(origins(path: descriptor.derivation)))) +// originsArray.append(.init(key: 2, value: .unsignedInt(UInt64(descriptor.fingerprint, radix: 16) ?? 0))) +// originsArray.append(.init(key: 3, value: .unsignedInt(UInt64(depth.hexString) ?? 0))) +// let originsWrapper = CBOR.orderedMap(originsArray) + + let originsWrapper: CBOR = .map([ + CBOR.unsigned(1): CBOR.array(origins(path: descriptor.derivation)), + CBOR.unsigned(2): CBOR.unsigned(UInt64(descriptor.fingerprint, radix: 16) ?? 0), + CBOR.unsigned(3): CBOR.unsigned(UInt64(depth.hexString) ?? 0) + ]) let useInfoWrapper:CBOR = .map([ - .unsignedInt(2) : .unsignedInt(cointype) + CBOR.unsigned(2) : CBOR.unsigned(cointype) ]) guard let hexValue = UInt64(parentFingerprint.hexString, radix: 16) else { return nil } - var hdkeyArray:[OrderedMapEntry] = [] - hdkeyArray.append(.init(key: 1, value: .boolean(false))) - hdkeyArray.append(.init(key: 2, value: .boolean(isPrivate))) - hdkeyArray.append(.init(key: 3, value: .byteString([UInt8](keyData)))) - hdkeyArray.append(.init(key: 4, value: .byteString([UInt8](chaincode)))) - hdkeyArray.append(.init(key: 5, value: .tagged(CBOR.Tag(rawValue: 305), useInfoWrapper))) - hdkeyArray.append(.init(key: 6, value: .tagged(CBOR.Tag(rawValue: 304), originsWrapper))) - hdkeyArray.append(.init(key: 8, value: .unsignedInt(hexValue))) +// var hdkeyArray:[OrderedMapEntry] = [] +// hdkeyArray.append(.init(key: 1, value: .boolean(false))) +// hdkeyArray.append(.init(key: 2, value: .boolean(isPrivate))) +// hdkeyArray.append(.init(key: 3, value: .byteString([UInt8](keyData)))) +// hdkeyArray.append(.init(key: 4, value: .byteString([UInt8](chaincode)))) +// hdkeyArray.append(.init(key: 5, value: .tagged(CBOR.Tag(rawValue: 305), useInfoWrapper))) +// hdkeyArray.append(.init(key: 6, value: .tagged(CBOR.Tag(rawValue: 304), originsWrapper))) +// hdkeyArray.append(.init(key: 8, value: .unsignedInt(hexValue))) + var hdkeyMap: CBOR = .map([ + CBOR.unsigned(1): CBOR(booleanLiteral: false), + CBOR.unsigned(2): CBOR(booleanLiteral: isPrivate), + CBOR.unsigned(3): CBOR.bytes(keyData), + CBOR.unsigned(4): CBOR.bytes(chaincode), + CBOR.unsigned(5): CBOR.tagged(305, useInfoWrapper), + CBOR.unsigned(6): CBOR.tagged(304, originsWrapper), + CBOR.unsigned(8): CBOR.unsigned(hexValue) + ]) - return CBOR.orderedMap(hdkeyArray) + return hdkeyMap } static func taggedHdKeyCbor(_ descriptor: Descriptor) -> CBOR? { guard let cbor = descriptorToHdKeyCbor(descriptor) else { return nil } - return .tagged(CBOR.Tag(rawValue: 303), cbor) + return CBOR.tagged(303, cbor) + //return .tagged(CBOR.Tag(rawValue: 303), cbor) } @@ -737,18 +786,25 @@ class URHelper { hdkeyArray.append(hdkey) } - var keyThreshholdArray:[OrderedMapEntry] = [] - keyThreshholdArray.append(.init(key: 1, value: .unsignedInt(UInt64(threshold)))) - keyThreshholdArray.append(.init(key: 2, value: .array(hdkeyArray))) - let keyThreshholdArrayCbor = CBOR.orderedMap(keyThreshholdArray) +// var keyThreshholdArray:[OrderedMapEntry] = [] +// keyThreshholdArray.append(.init(key: 1, value: .unsignedInt(UInt64(threshold)))) +// keyThreshholdArray.append(.init(key: 2, value: .array(hdkeyArray))) +// let keyThreshholdArrayCbor = CBOR.orderedMap(keyThreshholdArray) + + var keyThreshholdMap: CBOR = .map([ + CBOR.unsigned(1): CBOR.unsigned(UInt64(threshold)), + CBOR.unsigned(2): CBOR.array(hdkeyArray), + ]) + + var multisigTag: UInt64 - var multisigTag:CBOR.Tag! if descriptor.isBIP67 { - multisigTag = .init(rawValue: 407) + multisigTag = 407 } else { - multisigTag = .init(rawValue: 406) + multisigTag = 406 } - let taggedMsigCbor:CBOR = .tagged(multisigTag, keyThreshholdArrayCbor) + + let taggedMsigCbor:CBOR = .tagged(Tag(multisigTag), keyThreshholdMap) switch descriptor { case _ where descriptor.format == "P2WSH": @@ -806,14 +862,18 @@ class URHelper { static func cborOutputToCryptoAccount(_ cryptoOutputCbor: CBOR, _ descriptor: Descriptor) -> String? { guard let hexValue = UInt64(descriptor.fingerprint, radix: 16) else { return nil } +// var cborArray:[OrderedMapEntry] = [] +// cborArray.append(.init(key: 1, value: .unsignedInt(hexValue))) +// cborArray.append(.init(key: 2, value: .array([cryptoOutputCbor]))) - var cborArray:[OrderedMapEntry] = [] - cborArray.append(.init(key: 1, value: .unsignedInt(hexValue))) - cborArray.append(.init(key: 2, value: .array([cryptoOutputCbor]))) + var cborMap: CBOR = .map([ + CBOR.unsigned(1): CBOR.unsigned(hexValue), + CBOR.unsigned(2): CBOR.array([cryptoOutputCbor]) + ]) - let cbor = CBOR.orderedMap(cborArray) + //let cbor = CBOR.orderedMap(cborArray) - guard let rawUr = try? UR(type: "crypto-account", cbor: cbor) else { return nil } + guard let rawUr = try? UR(type: "crypto-account", cbor: cborMap) else { return nil } return UREncoder.encode(rawUr) } @@ -826,24 +886,24 @@ class URHelper { let processed = item.split(separator: "h") if let int = Int("\(processed[0])") { - let unsignedInt = CBOR.unsignedInt(UInt64(int)) + let unsignedInt = CBOR.unsigned(UInt64(int)) cborArray.append(unsignedInt) - cborArray.append(CBOR.boolean(true)) + cborArray.append(CBOR(booleanLiteral: true)) } } else if item.contains("'") { let processed = item.split(separator: "'") if let int = Int("\(processed[0])") { - let unsignedInt = CBOR.unsignedInt(UInt64(int)) + let unsignedInt = CBOR.unsigned(UInt64(int)) cborArray.append(unsignedInt) - cborArray.append(CBOR.boolean(true)) + cborArray.append(CBOR(booleanLiteral: true)) } } else { if let int = Int("\(item)") { - let unsignedInt = CBOR.unsignedInt(UInt64(int)) + let unsignedInt = CBOR.unsigned(UInt64(int)) cborArray.append(unsignedInt) - cborArray.append(CBOR.boolean(false)) + cborArray.append(CBOR(booleanLiteral: false)) } } } diff --git a/FullyNoded/Helpers/UR/UseInfo.swift b/FullyNoded/Helpers/UR/UseInfo.swift index 7c47e26c..9ae9f309 100644 --- a/FullyNoded/Helpers/UR/UseInfo.swift +++ b/FullyNoded/Helpers/UR/UseInfo.swift @@ -50,17 +50,19 @@ struct UseInfo { } var cbor: CBOR { - var a: [OrderedMapEntry] = [] + var a: Map = [:] if asset != .btc { - a.append(.init(key: 1, value: asset.cbor)) + //a.append(.init(key: 1, value: asset.cbor)) + a.insert(CBOR.unsigned(1), asset.cbor) } if network != .mainnet { - a.append(.init(key: 2, value: network.cbor)) + //a.append(.init(key: 2, value: network.cbor)) + a.insert(CBOR.unsigned(2), network.cbor) } - return CBOR.orderedMap(a) + return CBOR.map(a) } var taggedCBOR: CBOR { @@ -73,14 +75,14 @@ struct UseInfo { } let asset: Asset - if let rawAsset = pairs[1] { + if let rawAsset = pairs.get(1) { asset = try Asset(cbor: rawAsset) } else { asset = .btc } let network: Network_ - if let rawNetwork = pairs[2] { + if let rawNetwork = pairs.get(2) { network = try Network_(cbor: rawNetwork) } else { network = .mainnet diff --git a/FullyNoded/Node Logic/NodeLogic.swift b/FullyNoded/Node Logic/NodeLogic.swift index 96afdac5..172a556b 100644 --- a/FullyNoded/Node Logic/NodeLogic.swift +++ b/FullyNoded/Node Logic/NodeLogic.swift @@ -287,7 +287,7 @@ class NodeLogic { dateFormatter.dateFormat = "MMM-dd-yyyy HH:mm" let dateString = dateFormatter.string(from: date) - let amountBtc = amountSat.doubleValue.satsToBtc + let amountBtc = amountSat.doubleValue.btcBalanceWithSpaces let fxRate = UserDefaults.standard.object(forKey: "fxRate") as? Double ?? 0.0 let amountFiat = (amountBtc.doubleValue * fxRate).balanceText @@ -349,7 +349,7 @@ class NodeLogic { dateFormatter.dateFormat = "MMM-dd-yyyy HH:mm" let dateString = dateFormatter.string(from: date) - let amountBtc = amt_paid_sat.satsToBtc.avoidNotation + let amountBtc = amt_paid_sat.satsToBtc.btcBalanceWithSpaces let fxRate = UserDefaults.standard.object(forKey: "fxRate") as? Double ?? 0.0 let amountFiat = (amountBtc.doubleValue * fxRate).balanceText @@ -444,7 +444,7 @@ class NodeLogic { if status == "SUCCEEDED" { - let amountBtc = amount.satsToBtc.avoidNotation + let amountBtc = amount.satsToBtc.btcBalanceWithSpaces let fxRate = UserDefaults.standard.object(forKey: "fxRate") as? Double ?? 0.0 let amountFiat = (amountBtc.doubleValue * fxRate).balanceText @@ -533,7 +533,7 @@ class NodeLogic { if status == "paid" { let amountSats = Double(amountMsat) / 1000.0 - let amountBtc = "\(amountSats)".satsToBtc.avoidNotation + let amountBtc = "\(amountSats)".satsToBtc.btcBalanceWithSpaces let fxRate = UserDefaults.standard.object(forKey: "fxRate") as? Double ?? 0.0 let amountFiat = (amountBtc.doubleValue * fxRate).balanceText @@ -605,7 +605,7 @@ class NodeLogic { if status != "failed" { let amountSats = Double(amountMsat) / 1000.0 - let amountBtc = "\(amountSats)".satsToBtc.avoidNotation + let amountBtc = "\(amountSats)".satsToBtc.btcBalanceWithSpaces let fxRate = UserDefaults.standard.object(forKey: "fxRate") as? Double ?? 0.0 let amountFiat = (amountBtc.doubleValue * fxRate).balanceText @@ -751,7 +751,7 @@ class NodeLogic { let dateString = dateFormatter.string(from: date) let amountSats = amountString.btcToSats - let amountBtc = amountString.doubleValue.avoidNotation + let amountBtc = amountString.doubleValue.btcBalanceWithSpaces let fxRate = UserDefaults.standard.object(forKey: "fxRate") as? Double ?? 0.0 let amountFiat = (amountBtc.doubleValue * fxRate).balanceText diff --git a/FullyNoded/Objects/FullyNoded/Descriptor.swift b/FullyNoded/Objects/FullyNoded/Descriptor.swift index e9932344..d0bdafe5 100644 --- a/FullyNoded/Objects/FullyNoded/Descriptor.swift +++ b/FullyNoded/Objects/FullyNoded/Descriptor.swift @@ -5,6 +5,7 @@ // Created by Peter on 15/02/20. // Copyright © 2020 Blockchain Commons, LLC. All rights reserved. // +import LibWally public struct Descriptor: CustomStringConvertible { @@ -245,6 +246,7 @@ public struct Descriptor: CustomStringConvertible { } else { dictionary["isMulti"] = false + dictionary["mOfNType"] = "Single Sig" if descriptor.contains("[") && descriptor.contains("]") { let arr1 = descriptor.split(separator: "[") @@ -263,6 +265,9 @@ public struct Descriptor: CustomStringConvertible { dictionary["accountXpub"] = "\(extendedKey.replacingOccurrences(of: ")", with: ""))" } else if extendedKey.contains("tprv") || extendedKey.contains("xprv") { dictionary["accountXprv"] = "\(extendedKey.replacingOccurrences(of: ")", with: ""))" + if let hdkey = try? HDKey(base58: String(extendedKey)) { + dictionary["accountXpub"] = hdkey.xpub + } } else { let subarray = extendedKey.split(separator: "#") if subarray.count == 2 { diff --git a/FullyNoded/View Controllers/Home/Incoming/Invoice/InvoiceViewController.swift b/FullyNoded/View Controllers/Home/Incoming/Invoice/InvoiceViewController.swift index 00acb939..84216b72 100644 --- a/FullyNoded/View Controllers/Home/Incoming/Invoice/InvoiceViewController.swift +++ b/FullyNoded/View Controllers/Home/Incoming/Invoice/InvoiceViewController.swift @@ -142,7 +142,7 @@ class InvoiceViewController: UIViewController, UITextFieldDelegate { @IBAction func copyAddressAction(_ sender: Any) { UIPasteboard.general.string = addressString - displayAlert(viewController: self, isError: false, message: "address copied ✓") + showAlert(vc: self, title: "", message: "Address text copied ✓") } @@ -159,7 +159,7 @@ class InvoiceViewController: UIViewController, UITextFieldDelegate { @IBAction func copyQrAction(_ sender: Any) { UIPasteboard.general.image = self.qrView.image - displayAlert(viewController: self, isError: false, message: "qr copied ✓") + showAlert(vc: self, title: "", message: "QR copied ✓") } @IBAction func shareInvoiceTextAction(_ sender: Any) { @@ -168,7 +168,7 @@ class InvoiceViewController: UIViewController, UITextFieldDelegate { @IBAction func copyInvoiceTextAction(_ sender: Any) { UIPasteboard.general.string = invoiceText.text - displayAlert(viewController: self, isError: false, message: "invoice text copied ✓") + showAlert(vc: self, title: "", message: "Invoice text copied ✓") } @IBAction func generateLightningAction(_ sender: Any) { diff --git a/FullyNoded/View Controllers/Home/Outgoing/Raw Tx/CreateRawTxViewController.swift b/FullyNoded/View Controllers/Home/Outgoing/Raw Tx/CreateRawTxViewController.swift index a8a525ed..984789ad 100644 --- a/FullyNoded/View Controllers/Home/Outgoing/Raw Tx/CreateRawTxViewController.swift +++ b/FullyNoded/View Controllers/Home/Outgoing/Raw Tx/CreateRawTxViewController.swift @@ -32,7 +32,10 @@ class CreateRawTxViewController: UIViewController, UITextFieldDelegate, UITableV var invoiceString = "" let fiatCurrency = UserDefaults.standard.object(forKey: "currency") as? String ?? "USD" var isFidelity = false + var balance = "" + + @IBOutlet weak private var balanceLabel: UILabel! @IBOutlet weak private var batchOutlet: UIButton! @IBOutlet weak private var lightningWithdrawOutlet: UIButton! @IBOutlet weak private var miningTargetLabel: UILabel! @@ -77,6 +80,7 @@ class CreateRawTxViewController: UIViewController, UITextFieldDelegate, UITableV outputsTable.alpha = 0 addressImageView.alpha = 0 slider.isContinuous = false + balanceLabel.text = "Available: \(balance)" addTapGesture() sliderViewBackground.layer.cornerRadius = 8 @@ -1972,8 +1976,35 @@ class CreateRawTxViewController: UIViewController, UITextFieldDelegate, UITableV } func getRawTx() { + + func createNow() { + CreatePSBT.create(inputs: self.inputs, outputs: self.outputs) { [weak self] (psbt, rawTx, errorMessage) in + guard let self = self else { return } + + self.spinner.removeConnectingView() + + if rawTx != nil { + self.rawTxSigned = rawTx! + self.showRaw(raw: rawTx!) + + } else if psbt != nil { + self.rawTxUnsigned = psbt! + self.showRaw(raw: psbt!) + + } else { + self.outputs.removeAll() + DispatchQueue.main.async { + self.outputsTable.reloadData() + } + + showAlert(vc: self, title: "Error", message: errorMessage ?? "unknown error creating transaction") + } + } + } + activeWallet { wallet in guard let wallet = wallet else { + createNow() return } @@ -1981,31 +2012,9 @@ class CreateRawTxViewController: UIViewController, UITextFieldDelegate, UITableV self.jmWallet = wallet self.chooseMixdepthToSpendFrom() } else { - CreatePSBT.create(inputs: self.inputs, outputs: self.outputs) { [weak self] (psbt, rawTx, errorMessage) in - guard let self = self else { return } - - self.spinner.removeConnectingView() - - if rawTx != nil { - self.rawTxSigned = rawTx! - self.showRaw(raw: rawTx!) - - } else if psbt != nil { - self.rawTxUnsigned = psbt! - self.showRaw(raw: psbt!) - - } else { - self.outputs.removeAll() - DispatchQueue.main.async { - self.outputsTable.reloadData() - } - - showAlert(vc: self, title: "Error", message: errorMessage ?? "unknown error creating transaction") - } - } + createNow() } } - } func textFieldDidBeginEditing(_ textField: UITextField) { diff --git a/FullyNoded/View Controllers/Home/Outgoing/UTXO's/UTXOCell.swift b/FullyNoded/View Controllers/Home/Outgoing/UTXO's/UTXOCell.swift index a7d8693a..e976ab69 100644 --- a/FullyNoded/View Controllers/Home/Outgoing/UTXO's/UTXOCell.swift +++ b/FullyNoded/View Controllers/Home/Outgoing/UTXO's/UTXOCell.swift @@ -121,7 +121,7 @@ class UTXOCell: UITableViewCell { if isFiat { amountLabel.text = utxo.amountFiat ?? "missing fx rate" } else if isBtc { - amountLabel.text = amount.btc + amountLabel.text = amount.btcBalanceWithSpaces } else if isSats { amountLabel.text = amount.sats } diff --git a/FullyNoded/View Controllers/Home/Outgoing/UTXO's/UTXOViewController.swift b/FullyNoded/View Controllers/Home/Outgoing/UTXO's/UTXOViewController.swift index b1c4efc0..2f1b4c2e 100644 --- a/FullyNoded/View Controllers/Home/Outgoing/UTXO's/UTXOViewController.swift +++ b/FullyNoded/View Controllers/Home/Outgoing/UTXO's/UTXOViewController.swift @@ -1447,7 +1447,17 @@ extension UTXOViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: UTXOCell.identifier, for: indexPath) as! UTXOCell let utxo = unlockedUtxos[indexPath.section] - cell.configure(utxo: utxo, isLocked: false, fxRate: fxRate, isSats: isSats, isBtc: isBtc, isFiat: isFiat, delegate: self) + + cell.configure( + utxo: utxo, + isLocked: false, + fxRate: fxRate, + isSats: isSats, + isBtc: isBtc, + isFiat: isFiat, + delegate: self + ) + return cell } diff --git a/FullyNoded/View Controllers/QRDisplayerViewController.swift b/FullyNoded/View Controllers/QRDisplayerViewController.swift index 53eacda3..dca57a55 100644 --- a/FullyNoded/View Controllers/QRDisplayerViewController.swift +++ b/FullyNoded/View Controllers/QRDisplayerViewController.swift @@ -8,11 +8,13 @@ import UIKit import URKit +import Bbqr class QRDisplayerViewController: UIViewController { var text = "" var psbt = "" + var txn = "" var tapQRGesture = UITapGestureRecognizer() var tapTextViewGesture = UITapGestureRecognizer() var headerText = "" @@ -21,6 +23,7 @@ class QRDisplayerViewController: UIViewController { var spinner = ConnectingView() let qrGenerator = QRGenerator() var isPaying = false + var isBbqr = false private var encoder:UREncoder! private var timer: Timer? @@ -28,6 +31,7 @@ class QRDisplayerViewController: UIViewController { private var ur: UR! private var partIndex = 0 + @IBOutlet weak var animateOutlet: UIButton! @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var textView: UITextView! @IBOutlet weak var headerLabel: UILabel! @@ -42,8 +46,28 @@ class QRDisplayerViewController: UIViewController { textView.text = descriptionText tapQRGesture = UITapGestureRecognizer(target: self, action: #selector(shareQRCode(_:))) imageView.addGestureRecognizer(tapQRGesture) + animateOutlet.alpha = 0 - if psbt != "" { + if isBbqr { + var parts: [String]? = [] + + if psbt != "" { + parts = try? split(string: psbt) + } + + if txn != "" { + parts = try? split(string: txn) + } + + if text != "" { + parts = try? split(string: text) + } + + if let parts = parts { + showBbqrParts(bbQrparts: parts) + } + } else if psbt != "" { + animateOutlet.alpha = 0 spinner.addConnectingView(vc: self, description: "loading QR parts...") imageView.isUserInteractionEnabled = false @@ -53,116 +77,39 @@ class QRDisplayerViewController: UIViewController { convertPsbtToUrParts() } - } else if !isPaying { - imageView.image = qR() - } - - if isPaying { - getPaymentAddress() - } - } - - private func getPaymentAddress() { - guard let data = KeyChain.getData("paymentAddress") else { - - guard let paymentAddress = Keys.donationAddress() else { return } - - guard KeyChain.set(paymentAddress.dataUsingUTF8StringEncoding, forKey: "paymentAddress") else { - return - } - - getPaid(paymentAddress) - - return - } - - let paymentAddress = data.utf8String ?? "" - getPaid(paymentAddress) - } - - private func getPaid(_ address: String) { - FiatConverter.sharedInstance.getFxRate { [weak self] fxRate in - guard let self = self, let fxRate = fxRate else { return } - - let btcAmount = 1.0 / (fxRate / 20.0) - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.text = "bitcoin:\(address)?amount=\(btcAmount.avoidNotation)&label=FullyNoded-Payment" - - self.imageView.image = self.qR() - - self.spinner.removeConnectingView() - - showAlert(vc: self, title: "Thank you for supporting Fully Noded", message: "In order to use Fully Noded via direct download a donation of $20 in btc is suggested. You can scan this QR with any wallet to automatically pay the suggested amount, this address is unique to you and will not change, that way you can pay whenever you want.\n\nThe app has taken years of hard work, your support will help make Fully Noded even better ensuring its long term survival and evolution to be the best it can possibly be.\n\nOnce the payment is made you will have full lifetime access to the app.") + } else { + if text.lowercased().hasPrefix("ur:") { + animateOutlet.alpha = 1 } - DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in - guard let self = self else { return } - - self.checkIfPaymentReceived(address) - } + if txn != "" { + imageView.image = qR(text: txn) + } else if text != "" { + imageView.image = qR(text: text) + } } } - private func checkIfPaymentReceived(_ address: String) { - let blockstreamUrl = "http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/api/address/" + address - - guard let url = URL(string: blockstreamUrl) else { return } - - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue("text/plain", forHTTPHeaderField: "Content-Type") + func split(string: String) throws -> [String] { + let large = Data(string.utf8) + + // EXAMPLE DEFAULT OPTIONS + // let options = defaultSplitOptions() + let options = SplitOptions(encoding: Encoding.zlib, minVersion: Version.v01, maxVersion: Version.v40) + var fileType: FileType = .unicodeText - let task = TorClient.sharedInstance.session.dataTask(with: request as URLRequest) { (data, response, error) in - - guard let urlContent = data else { - showAlert(vc: self, title: "Ooops", message: "There was an issue checking on payment status") - return - } - - guard let json = try? JSONSerialization.jsonObject(with: urlContent, options: JSONSerialization.ReadingOptions.mutableLeaves) as? NSDictionary else { - showAlert(vc: self, title: "Ooops", message: "There was an issue decoding the response when fetching payment status") - return - } - - var txCount = 0 - - if let chain_stats = json["chain_stats"] as? NSDictionary { - guard let count = chain_stats["tx_count"] as? Int else { return } - - txCount += count - } - - if let mempool_stats = json["mempool_stats"] as? NSDictionary { - guard let count = mempool_stats["tx_count"] as? Int else { return } - - txCount += count - } - - if txCount == 0 { - DispatchQueue.main.asyncAfter(deadline: .now() + 15.0) { [weak self] in - guard let self = self else { return } - - self.checkIfPaymentReceived(address) - } - - } else { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - let _ = KeyChain.set("hasPaid".dataUsingUTF8StringEncoding, forKey: "hasPaid") - - self.dismiss(animated: true) { - - showAlert(vc: self, title: "Thank you!", message: "Your support is greatly appreciated and will directly help making Fully Noded even better 💪") - } - } - } + if psbt != "" { + fileType = .psbt } - task.resume() + if txn != "" { + fileType = .transaction + } + + //let options = SplitOptions(encoding: Encoding.hex, minVersion: Version.v01, maxVersion: Version.v02) + let split = try Split.tryFromData(bytes: large, fileType: fileType, options: options) + + return split.parts() } @IBAction func closeAction(_ sender: Any) { @@ -171,7 +118,15 @@ class QRDisplayerViewController: UIViewController { } } - private func qR() -> UIImage { + @IBAction func animateAction(_ sender: Any) { + if text.lowercased().hasPrefix("ur:") { + guard let ur = URHelper.ur(text) else { return } + + animateUr(ur: ur) + } + } + + private func qR(text: String) -> UIImage { qrGenerator.textInput = text return qrGenerator.getQRCode() } @@ -201,8 +156,7 @@ class QRDisplayerViewController: UIViewController { imageView.image = qrGenerator.getQRCode() } - private func convertPsbtToUrParts() { - guard let b64 = Data(base64Encoded: psbt), let ur = URHelper.psbtUr(b64) else { return } + private func animateUr(ur: UR) { let encoder = UREncoder(ur, maxFragmentLen: 250) weak var timer: Timer? @@ -222,28 +176,29 @@ class QRDisplayerViewController: UIViewController { } } - private func convertBlindedPsbtToUrParts() { - guard let ur = try? UR(urString: psbt) else { return } - - let encoder = UREncoder(ur, maxFragmentLen: 250) - weak var timer: Timer? + private func convertPsbtToUrParts() { + guard let b64 = Data(base64Encoded: psbt), let ur = URHelper.psbtUr(b64) else { return } - timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] _ in + animateUr(ur: ur) + } + + private func showBbqrParts(bbQrparts: [String]) { + let _ = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { [weak self] _ in guard let self = self else { return } - let part = encoder.nextPart() - let index = encoder.seqNum - - if index <= encoder.seqLen { - self.parts.append(part.uppercased()) + if partIndex < bbQrparts.count { + showQR(bbQrparts[partIndex]) + partIndex += 1 } else { - self.spinner.removeConnectingView() - timer?.invalidate() - timer = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: #selector(self.animate), userInfo: nil, repeats: true) + partIndex = 0 } } } - + private func convertBlindedPsbtToUrParts() { + guard let ur = try? UR(urString: psbt) else { return } + + animateUr(ur: ur) + } } diff --git a/FullyNoded/View Controllers/QRScannerViewController.swift b/FullyNoded/View Controllers/QRScannerViewController.swift index 798abebd..e5db5f8a 100644 --- a/FullyNoded/View Controllers/QRScannerViewController.swift +++ b/FullyNoded/View Controllers/QRScannerViewController.swift @@ -9,6 +9,7 @@ import URKit import AVFoundation import UIKit +import Bbqr @available(macCatalyst 14.0, *) class QRScannerViewController: UIViewController { @@ -28,6 +29,7 @@ class QRScannerViewController: UIViewController { var onDoneBlock : ((String?) -> Void)? var fromSignAndVerify = Bool() var decoder:URDecoder! + var bbqrParts: [String] = [] private let spinner = ConnectingView() private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffect.Style.dark)) private var blurArray = [UIVisualEffectView]() @@ -143,23 +145,23 @@ class QRScannerViewController: UIViewController { } } - private func processUrPsbt(text: String) { - if text.uppercased().hasPrefix("UR:BYTES") { + private func processUrQr(text: String) { + if text.uppercased().hasPrefix("UR:PSBT") { guard decoder.result == nil else { - guard let result = try? decoder.result?.get() else { return } + guard let result = try? decoder.result?.get(), let psbt = URHelper.psbtUrToBase64Text(result) else { return } hasScanned = true - stopScanning(result.qrString) + stopScanning(psbt) return } decoder.receivePart(text.lowercased()) - let expectedParts = decoder.expectedPartCount ?? 0 + let expectedParts = decoder.expectedFragmentCount ?? 0//.expectedPartCount ?? 0 guard expectedParts != 0 else { - guard let result = try? decoder.result?.get() else { return } + guard let result = try? decoder.result?.get(), let psbt = URHelper.psbtUrToBase64Text(result) else { return } hasScanned = true - stopScanning(result.qrString) + stopScanning(psbt) return } @@ -167,20 +169,20 @@ class QRScannerViewController: UIViewController { updateProgress(percentageCompletion, self.decoder.estimatedPercentComplete) } else { guard decoder.result == nil else { - guard let result = try? decoder.result?.get(), let psbt = URHelper.psbtUrToBase64Text(result) else { return } + guard let result = try? decoder.result?.get() else { return } hasScanned = true - stopScanning(psbt) + stopScanning(result.qrString) return } decoder.receivePart(text.lowercased()) - let expectedParts = decoder.expectedPartCount ?? 0 + let expectedParts = decoder.expectedFragmentCount ?? 0//.expectedPartCount ?? 0 guard expectedParts != 0 else { - guard let result = try? decoder.result?.get(), let psbt = URHelper.psbtUrToBase64Text(result) else { return } + guard let result = try? decoder.result?.get() else { return } hasScanned = true - stopScanning(psbt) + stopScanning(result.qrString) return } @@ -188,7 +190,6 @@ class QRScannerViewController: UIViewController { updateProgress(percentageCompletion, self.decoder.estimatedPercentComplete) } // Stop if we're already done with the decode. - } private func updateProgress(_ progressText: String, _ progressDoub: Double) { @@ -285,13 +286,61 @@ class QRScannerViewController: UIViewController { } } + func continousJoiner(parts: [String]) throws -> ((psbt: String?, descriptor: String?)) { + let continousJoiner = ContinuousJoiner() + + for part in parts { + switch try continousJoiner.addPart(part: part) { + case .notStarted: + #if DEBUG + print("not started") + #endif + + case .inProgress(let partsLeft): + #if DEBUG + print("added item, \(partsLeft) parts left") + #endif + hasScanned = false + + case .complete(let joined): + hasScanned = true + let s = String(decoding: joined.data(), as: UTF8.self) + if s.hasPrefix("psbt") { + stopScanning(joined.data().base64EncodedString()) + } else { + stopScanning(s) + } + } + } + + return ((nil, nil)) + } + + private func processBBQr(text: String) { + bbqrParts.append(text) + + guard let result = try? continousJoiner(parts: bbqrParts) else { return } + + #if DEBUG + print("BBQr result: \(result)") + #endif + } + private func process(text: String) { let lowercased = text.lowercased() + #if DEBUG + print("text: \(text)") + #endif + if fromSignAndVerify { - hasScanned = false - - if Keys.validTx(text) { + if lowercased.hasPrefix("ur:crypto-psbt") || lowercased.hasPrefix("ur:bytes") { + processUrQr(text: text) + + } else if text.hasPrefix("B$") { + processBBQr(text: text) + + } else if Keys.validTx(text) { // its a raw transaction hasScanned = true stopScanning(text) @@ -302,21 +351,26 @@ class QRScannerViewController: UIViewController { } else if text.hasPrefix("p") { // could be a specter animated psbt parseSpecterAnimatedQr(text) - } else if lowercased.hasPrefix("ur:crypto-psbt") || lowercased.hasPrefix("ur:bytes") { - processUrPsbt(text: text) } else { spinner.removeConnectingView() - showAlert(vc: self, title: "Unrecognized format", message: "That is an unrecognized transaction format, please reach out to us so we can add compatibility.") + showAlert(vc: self, + title: "Unrecognized format", + message: "That is an unrecognized transaction format, please reach out to us so we can add compatibility.") } } else if isImporting { - if lowercased.hasPrefix("ur:bytes") { - hasScanned = false - processUrPsbt(text: lowercased) + if text.hasPrefix("B$") { + processBBQr(text: text) + } else if lowercased.hasPrefix("ur:") { + processUrQr(text: lowercased) } else { - hasScanned = true - stopScanning(text) + DispatchQueue.main.async { [unowned vc = self] in + vc.dismiss(animated: true) { + vc.stopScanner() + vc.onDoneBlock!(text) + } + } } } else if isQuickConnect { @@ -350,20 +404,17 @@ class QRScannerViewController: UIViewController { private func configureCloseButton() { closeButton.frame = CGRect(x: view.frame.midX - 15, y: view.frame.maxY - 150, width: 30, height: 30) - closeButton.showsTouchWhenHighlighted = true closeButton.setImage(UIImage(named: "Image-10"), for: .normal) } private func configureTorchButton() { torchButton.frame = CGRect(x: 17.5, y: 17.5, width: 35, height: 35) torchButton.setImage(UIImage(named: "strobe.png"), for: .normal) - torchButton.showsTouchWhenHighlighted = true addShadow(view: torchButton) } private func configureUploadButton() { uploadButton.frame = CGRect(x: 17.5, y: 17.5, width: 35, height: 35) - uploadButton.showsTouchWhenHighlighted = true uploadButton.setImage(UIImage(named: "images.png"), for: .normal) addShadow(view: uploadButton) } @@ -490,14 +541,13 @@ extension QRScannerViewController: AVCaptureMetadataOutputObjectsDelegate { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.stopScanner() - //self.avCaptureSession.stopRunning() let impact = UIImpactFeedbackGenerator() impact.impactOccurred() AudioServicesPlaySystemSound(1103) } hasScanned = true - + process(text: stringURL) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in diff --git a/FullyNoded/View Controllers/VerifyTransactionViewController.swift b/FullyNoded/View Controllers/VerifyTransactionViewController.swift index e7533a7e..0069c461 100644 --- a/FullyNoded/View Controllers/VerifyTransactionViewController.swift +++ b/FullyNoded/View Controllers/VerifyTransactionViewController.swift @@ -52,6 +52,7 @@ class VerifyTransactionViewController: UIViewController, UINavigationControllerD var qrCodeStringToExport = "" var blind = false var processedPsbt:String? + var isBBQr = false @IBOutlet weak private var verifyTable: UITableView! @IBOutlet weak private var exportButtonOutlet: UIButton! @@ -480,7 +481,7 @@ class VerifyTransactionViewController: UIViewController, UINavigationControllerD self.exportPsbt(blindedpsbt: ur.qrString, plainText: nil) })) - alert.addAction(UIAlertAction(title: "Plain text", style: .default, handler: { [weak self] action in + alert.addAction(UIAlertAction(title: "Unencrypted", style: .default, handler: { [weak self] action in guard let self = self else { return } self.exportPsbt(blindedpsbt: nil, plainText: self.unsignedPsbt) @@ -2478,7 +2479,13 @@ class VerifyTransactionViewController: UIViewController, UINavigationControllerD self.shareText(itemToExport) })) - alert.addAction(UIAlertAction(title: "QR", style: .default, handler: { action in + alert.addAction(UIAlertAction(title: "UR QR", style: .default, handler: { action in + self.qrCodeStringToExport = itemToExport + self.exportAsQR() + })) + + alert.addAction(UIAlertAction(title: "BBQr QR", style: .default, handler: { action in + self.isBBQr = true self.qrCodeStringToExport = itemToExport self.exportAsQR() })) @@ -2679,8 +2686,9 @@ class VerifyTransactionViewController: UIViewController, UINavigationControllerD } } - if segue.identifier == "segueToExportPsbtAsQr" { + if segue.identifier == "segueToExportPsbtAsQr" { if let vc = segue.destination as? QRDisplayerViewController { + vc.isBbqr = self.isBBQr if self.qrCodeStringToExport != "" { vc.psbt = self.qrCodeStringToExport @@ -2690,11 +2698,15 @@ class VerifyTransactionViewController: UIViewController, UINavigationControllerD vc.headerText = "Encrypted PSBT" vc.descriptionText = "Pass this psbt to your signer or to others to create a collaborative batch transaction." } else { - vc.headerText = "PSBT" + if isBBQr { + vc.headerText = "BBQr PSBT" + } else { + vc.headerText = "PSBT" + } vc.descriptionText = "This psbt still needs more signatures to be complete, you can share it with another signer." } } else if signedRawTx != "" { - vc.text = signedRawTx + vc.txn = signedRawTx vc.headerIcon = UIImage(systemName: "square.and.arrow.up") vc.headerText = "Signed Transaction" vc.descriptionText = "You can save this signed transaction and broadcast it later or share it with someone else." @@ -2842,7 +2854,6 @@ extension VerifyTransactionViewController: UITableViewDelegate { } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() header.backgroundColor = UIColor.clear header.frame = CGRect(x: 0, y: 0, width: view.frame.size.width - 32, height: 50) @@ -2882,7 +2893,6 @@ extension VerifyTransactionViewController: UITableViewDelegate { copyButton.addTarget(self, action: #selector(copyTxid), for: .touchUpInside) copyButton.frame = CGRect(x: header.frame.maxX - 70, y: 0, width: 50, height: 50) copyButton.center.y = textLabel.center.y - copyButton.showsTouchWhenHighlighted = true header.addSubview(copyButton) case 4: diff --git a/FullyNoded/View Controllers/Wallets/ActiveWalletViewController.swift b/FullyNoded/View Controllers/Wallets/ActiveWalletViewController.swift index efc96a98..ec06de5d 100644 --- a/FullyNoded/View Controllers/Wallets/ActiveWalletViewController.swift +++ b/FullyNoded/View Controllers/Wallets/ActiveWalletViewController.swift @@ -536,7 +536,7 @@ class ActiveWalletViewController: UIViewController { if onchainBalanceBtc == "" || onchainBalanceBtc == "0.0" { - onchainBalanceBtc = "0.00000000" + onchainBalanceBtc = "0.00 000 000" } if isBtc { @@ -565,7 +565,7 @@ class ActiveWalletViewController: UIViewController { iconImageView.image = .init(systemName: "bolt") if offchainBalanceBtc == "" { - offchainBalanceBtc = "0.00000000" + offchainBalanceBtc = "0.00 000 000" } if isBtc { offchainBalanceLabel.text = offchainBalanceBtc @@ -590,7 +590,7 @@ class ActiveWalletViewController: UIViewController { let onchainBalanceLabel = cell.viewWithTag(1) as! UILabel if offchainBalanceBtc == "" { - offchainBalanceBtc = "0.00000000" + offchainBalanceBtc = "0.00 000 000" } if isBtc { offchainBalanceLabel.text = offchainBalanceBtc @@ -605,7 +605,7 @@ class ActiveWalletViewController: UIViewController { offchainBalanceView.alpha = 1 if onchainBalanceBtc == "" || onchainBalanceBtc == "0.0" { - onchainBalanceBtc = "0.00000000" + onchainBalanceBtc = "0.00 000 000" } if isBtc { @@ -765,7 +765,7 @@ class ActiveWalletViewController: UIViewController { var amountText = "" if isBtc { - amountText = amountBtc.btc + amountText = amountBtc } else if isSats { amountText = amountSats.sats } else if isFiat { @@ -783,7 +783,7 @@ class ActiveWalletViewController: UIViewController { var amountText = "" if isBtc { - amountText = "+" + amountBtc.btc + amountText = "+" + amountBtc } else if isSats { amountText = "+" + amountSats.sats } else if isFiat { @@ -1034,7 +1034,7 @@ class ActiveWalletViewController: UIViewController { } DispatchQueue.main.async { - self.onchainBalanceBtc = String(balance) + self.onchainBalanceBtc = balance.btcBalanceWithSpaces self.onchainBalanceSats = balance.sats.replacingOccurrences(of: " sats", with: "") if let exchangeRate = self.fxRate { @@ -1091,7 +1091,7 @@ class ActiveWalletViewController: UIViewController { DispatchQueue.main.async { [weak self] in guard let self = self else { return } totalBalance = Double(round(100000000 * totalBalance) / 100000000) - self.onchainBalanceBtc = String(totalBalance) + self.onchainBalanceBtc = totalBalance.btcBalanceWithSpaces self.onchainBalanceSats = totalBalance.sats.replacingOccurrences(of: " sats", with: "") self.sectionZeroLoaded = true self.walletTable.reloadSections(IndexSet.init(arrayLiteral: 0), with: .fade) @@ -1510,6 +1510,21 @@ class ActiveWalletViewController: UIViewController { override func prepare(for segue: UIStoryboardSegue, sender: Any?) { switch segue.identifier { + + case "spendFromWallet": + guard let vc = segue.destination as? CreateRawTxViewController else { fallthrough } + + if isBtc { + vc.balance = onchainBalanceBtc + } + + if isSats { + vc.balance = onchainBalanceSats + } + + if isFiat { + vc.balance = onchainBalanceFiat + } case "segueToInvoice": guard let vc = segue.destination as? InvoiceViewController else { fallthrough } diff --git a/FullyNoded/View Controllers/Wallets/CreateFullyNodedWalletViewController.swift b/FullyNoded/View Controllers/Wallets/CreateFullyNodedWalletViewController.swift index c847dab0..5b9d48e0 100644 --- a/FullyNoded/View Controllers/Wallets/CreateFullyNodedWalletViewController.swift +++ b/FullyNoded/View Controllers/Wallets/CreateFullyNodedWalletViewController.swift @@ -910,7 +910,10 @@ class CreateFullyNodedWalletViewController: UIViewController, UINavigationContro return } - // needs to check AM too + #if(DEBUG) + print("item: \(item)") + #endif + self.processImportedString(item) } } diff --git a/FullyNoded/View Controllers/Wallets/CreateMultisigViewController.swift b/FullyNoded/View Controllers/Wallets/CreateMultisigViewController.swift index 81d84d6c..d1b3a385 100644 --- a/FullyNoded/View Controllers/Wallets/CreateMultisigViewController.swift +++ b/FullyNoded/View Controllers/Wallets/CreateMultisigViewController.swift @@ -16,12 +16,13 @@ class CreateMultisigViewController: UIViewController, UITextViewDelegate, UIText var m = Int() var n = Int() var keysString = "" - var isDone = Bool() + var isDone = false var cosigner: Descriptor? var keys = [[String:String]]() var alertStyle = UIAlertController.Style.alert var multiSigAccountDesc = "" var qrToExport = "" + var isBbqr = false @IBOutlet weak var derivationField: UITextField! @IBOutlet weak var fingerprintField: UITextField! @@ -154,12 +155,22 @@ class CreateMultisigViewController: UIViewController, UITextViewDelegate, UIText } @IBAction func createButton(_ sender: Any) { - if keys.count > 1 { - promptToCreate() + if !isDone { + if keys.count > 1 { + promptToCreate() + } else { + showAlert(vc: self, title: "Add more cosigners first.", message: "Creating a multi-sig wallet with one cosigner is pointless...") + } } else { - showAlert(vc: self, title: "Add more cosigners first.", message: "Creating a multi-sig wallet with one cosigner is pointless...") + DispatchQueue.main.async { + NotificationCenter.default.post(name: .refreshWallet, object: nil, userInfo: nil) + if self.navigationController != nil { + self.navigationController?.popToRootViewController(animated: true) + } else { + self.dismiss(animated: true, completion: nil) + } + } } - } var cointType: String { @@ -268,13 +279,18 @@ class CreateMultisigViewController: UIViewController, UITextViewDelegate, UIText private func exportWallet(mofn: String) { isDone = true + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.createOutlet.setTitle("Done", for: .normal) + } + DispatchQueue.main.async { [weak self] in guard let self = self else { return } var message = "The wallet has been activated and the wallet view is refreshing, tap done to go back" var text = "" - if self.cosigner != nil { message = "Export the wallet as a text file (compatible with Coldcard) or QR code (compatible with Passport, Sparrow, Blue Wallet and more)." text = """ @@ -287,7 +303,6 @@ class CreateMultisigViewController: UIViewController, UITextViewDelegate, UIText """ self.textView.text = text - } self.spinner.removeConnectingView() @@ -303,7 +318,7 @@ class CreateMultisigViewController: UIViewController, UITextViewDelegate, UIText self.export(text: text) })) - alert.addAction(UIAlertAction(title: "Export QR Code (Passport, Sparrow, Blue)", style: .default, handler: { action in + alert.addAction(UIAlertAction(title: "Export UR QR Code", style: .default, handler: { action in guard let ur = URHelper.dataToUrBytes(text.utf8) else { showAlert(vc: self, title: "Error", message: "Unable to convert the text into a UR.") return @@ -318,6 +333,17 @@ class CreateMultisigViewController: UIViewController, UITextViewDelegate, UIText } })) + alert.addAction(UIAlertAction(title: "Export BBQr", style: .default, handler: { action in + self.qrToExport = text + self.isBbqr = true + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.performSegue(withIdentifier: "segueToExportMsig", sender: self) + } + })) + alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { action in DispatchQueue.main.async { NotificationCenter.default.post(name: .refreshWallet, object: nil, userInfo: nil) @@ -608,10 +634,15 @@ class CreateMultisigViewController: UIViewController, UITextViewDelegate, UIText case "segueToExportMsig": guard let vc = segue.destination as? QRDisplayerViewController else { return } - vc.psbt = self.qrToExport + vc.text = self.qrToExport + vc.isBbqr = self.isBbqr vc.headerIcon = UIImage(systemName: "square.and.arrow.up") - vc.headerText = "Multisig Wallet Export" - vc.descriptionText = "Scan this with Passport, Blue Wallet, Sparrow or other wallets which support UR QR to import the multisig wallet." + + if isBbqr { + vc.headerText = "Multisig Wallet BBQr" + } else { + vc.headerText = "Multisig Wallet UR Bytes" + } default: break diff --git a/FullyNoded/View Controllers/Wallets/ImportXpubViewController.swift b/FullyNoded/View Controllers/Wallets/ImportXpubViewController.swift index 29c3b101..9bd22235 100644 --- a/FullyNoded/View Controllers/Wallets/ImportXpubViewController.swift +++ b/FullyNoded/View Controllers/Wallets/ImportXpubViewController.swift @@ -8,12 +8,14 @@ import UIKit -class ImportXpubViewController: UIViewController, UITextFieldDelegate { +class ImportXpubViewController: UIViewController, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource { @IBOutlet weak var importOutlet: UIButton! @IBOutlet weak var labelField: UITextField! @IBOutlet weak var descriptorField: UILabel! + @IBOutlet var addressTableView: UITableView! + var addresses: [String] = [] var spinner = ConnectingView() var onDoneBlock:(((Bool)) -> Void)? var descriptor = "" @@ -21,6 +23,8 @@ class ImportXpubViewController: UIViewController, UITextFieldDelegate { override func viewDidLoad() { super.viewDidLoad() + addressTableView.delegate = self + addressTableView.dataSource = self importOutlet.clipsToBounds = true importOutlet.layer.cornerRadius = 8 labelField.delegate = self @@ -30,7 +34,11 @@ class ImportXpubViewController: UIViewController, UITextFieldDelegate { view.addGestureRecognizer(tapGesture) labelField.removeGestureRecognizer(tapGesture) + descriptorField.numberOfLines = 10 + descriptorField.lineBreakMode = .byTruncatingMiddle + addDescriptorToLabel(descriptor) + loadAddresses() } @objc func dismissKeyboard(_ sender: UITapGestureRecognizer) { @@ -46,6 +54,25 @@ class ImportXpubViewController: UIViewController, UITextFieldDelegate { self?.performSegue(withIdentifier: "segueToScanDescriptor", sender: self) } } + + private func loadAddresses() { + let p = Derive_Addresses(["descriptor": descriptor, "range": [0,4]]) + OnchainUtils.deriveAddresses(param: p) { [weak self] (addresses, message) in + guard let self = self else { return } + + guard let addresses = addresses else { return } + + for address in addresses { + self.addresses.append(address) + } + + DispatchQueue.main.async { + self.addressTableView.reloadData() + self.addressTableView.translatesAutoresizingMaskIntoConstraints = true + self.addressTableView.sizeToFit() + } + } + } private func importDescriptor() { guard descriptor != "" else { @@ -108,6 +135,37 @@ class ImportXpubViewController: UIViewController, UITextFieldDelegate { } } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return addresses.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "addressCell", for: indexPath) + let label = cell.viewWithTag(1) as! UILabel + label.text = "#\(indexPath.row) " + addresses[indexPath.row] + label.numberOfLines = 0 + label.sizeToFit() + label.translatesAutoresizingMaskIntoConstraints = true + + cell.sizeToFit() + cell.translatesAutoresizingMaskIntoConstraints = true + + return cell + + } + + private func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + return UITableView.automaticDimension + } + + func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } + +// func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { +// return 40 +// } + // MARK: - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation diff --git a/FullyNoded/View Controllers/Wallets/WalletDetailViewController.swift b/FullyNoded/View Controllers/Wallets/WalletDetailViewController.swift index 18fb74fd..33a8c98e 100644 --- a/FullyNoded/View Controllers/Wallets/WalletDetailViewController.swift +++ b/FullyNoded/View Controllers/Wallets/WalletDetailViewController.swift @@ -8,7 +8,6 @@ import UIKit - class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource, UITextViewDelegate, UINavigationControllerDelegate { @IBOutlet weak var detailTable: UITableView! @@ -20,13 +19,21 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable var addresses = "" var originalLabel = "" var backupQrImage: UIImage! - var exportWalletImage: UIImage! - var bbQr: UIImage! + var exportWalletImageCryptoOutput: UIImage! + var exportWalletImageURBytes: UIImage! + var exportWalletImageBBQr: UIImage! var backupText = "" + var backupFileText = "" var exportText = "" var textToShow = "" var json = "" var showReceive = 0 + var outputDescUr = "" + var urBytes = "" + var bbqrText = "" + var outputDescFormat = true + var urBytesFormat = false + var bbqrFormat = false var alertStyle = UIAlertController.Style.actionSheet private var labelField: UITextField! private var labelButton: UIButton! @@ -34,6 +41,7 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable private enum Section: Int { case label + case backupText case walletExport case backupQr case exportFile @@ -59,8 +67,11 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable if (UIDevice.current.userInterfaceIdiom == .pad) { alertStyle = UIAlertController.Style.alert } + spinner.addConnectingView(vc: self, description: "loading") - load() + DispatchQueue.global(qos: .background).async { [weak self] in + self?.load() + } } @IBAction func rescanAction(_ sender: Any) { @@ -194,20 +205,74 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable if let urOutput = URHelper.descriptorToUrOutput(Descriptor(self.wallet.receiveDescriptor)) { generator.textInput = urOutput.uppercased() - self.exportText = urOutput - self.exportWalletImage = generator.getQRCode() + self.outputDescUr = urOutput.uppercased() + self.exportWalletImageCryptoOutput = generator.getQRCode() } else { showAlert(vc: self, title: "", message: "Unable to convert your wallet to crypto-output.") } + + let receiveDescriptor = Descriptor(walletStruct.receiveDescriptor) + var keysText = "" + var deriv = "" + + if receiveDescriptor.isMulti { + let xfpArray = xfpArray(xfpString: receiveDescriptor.fingerprint) + + for (i, key) in receiveDescriptor.multiSigKeys.enumerated() { + keysText += "\(xfpArray[i]) : \(key)\n\n" + } + + let multisigDervArr = receiveDescriptor.derivationArray + let allItemsEqual = multisigDervArr.dropLast().allSatisfy { $0 == multisigDervArr.last } + + if allItemsEqual { + deriv = multisigDervArr[0] + } else { + deriv = "Multiple derivations!" + } + } else { + keysText = receiveDescriptor.fingerprint + " : " + receiveDescriptor.accountXpub + deriv = receiveDescriptor.derivation + } + + backupFileText = """ + Name: \(wallet.label) + Policy: \(receiveDescriptor.mOfNType) + Derivation: \(deriv) + Format: \(receiveDescriptor.format) + + \(keysText) + """ + + guard let urBytesCheck = URHelper.dataToUrBytes(backupFileText.utf8) else { + showAlert(vc: self, title: "Error", message: "Unable to convert the text into a UR.") + return + } + + urBytes = urBytesCheck.qrString + generator.textInput = urBytes + self.exportWalletImageURBytes = generator.getQRCode() + + bbqrText = wallet.receiveDescriptor + generator.textInput = bbqrText + exportWalletImageBBQr = generator.getQRCode() self.findSigner() self.getAddresses() + spinner.removeConnectingView() } } } } } + private func xfpArray(xfpString: String) -> [String] { + var fingerprintsString = xfpString + fingerprintsString = fingerprintsString.replacingOccurrences(of: "[", with: "") + fingerprintsString = fingerprintsString.replacingOccurrences(of: "]", with: "") + return fingerprintsString.components(separatedBy: ",") + } + private func findSigner() { CoreDataService.retrieveEntity(entityName: .signers) { [weak self] signers in guard let signers = signers, signers.count > 0 else { @@ -447,6 +512,50 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable } } + @objc func chooseExportFormatButtonAction(_ sender: UIButton) { + guard let sectionString = sender.restorationIdentifier, let section = Int(sectionString) else { return } + + switch Section(rawValue: section) { + case .walletExport: + if outputDescFormat { + outputDescFormat = false + bbqrFormat = false + urBytesFormat = true + } else if urBytesFormat { + urBytesFormat = false + bbqrFormat = true + outputDescFormat = false + } else { + outputDescFormat = true + bbqrFormat = false + urBytesFormat = false + } + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + detailTable.reloadSections(IndexSet(integer: Section.walletExport.rawValue), with: .fade) + } + + default: + break + } + } + + @objc func enlargeButtonAction(_ sender: UIButton) { + guard let sectionString = sender.restorationIdentifier, let section = Int(sectionString) else { return } + + switch Section(rawValue: section) { + case .walletExport: + textToShow = exportText + showQr() + + default: + break + } + } + + @objc func exportButtonAction(_ sender: UIButton) { guard let sectionString = sender.restorationIdentifier, let section = Int(sectionString) else { return } @@ -455,7 +564,17 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable exportItem(wallet.name) case .walletExport: - exportItem(exportWalletImage as Any) + if outputDescFormat { + exportItem(exportWalletImageCryptoOutput as Any) + } + + if urBytesFormat { + exportItem(exportWalletImageURBytes as Any) + } + + if bbqrFormat { + exportItem(exportWalletImageBBQr as Any) + } case .backupQr: exportItem(backupQrImage as Any) @@ -500,9 +619,6 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch Section(rawValue: indexPath.section) { - case .walletExport: - textToShow = exportText - showQr() case .backupQr: textToShow = backupText showQr() @@ -541,7 +657,16 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable labelButton = (cell.viewWithTag(2) as! UIButton) labelButton.addTarget(self, action: #selector(startEditingLabel), for: .touchUpInside) - labelButton.showsTouchWhenHighlighted = true + + return cell + } + + private func backupTextCell(_ indexPath: IndexPath) -> UITableViewCell { + let cell = detailTable.dequeueReusableCell(withIdentifier: "backupTextCell", for: indexPath) + configureCell(cell) + + let textView = cell.viewWithTag(1) as! UITextView + textView.text = backupFileText return cell } @@ -606,7 +731,7 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable field.layer.borderColor = UIColor.clear.cgColor let increaseButton = cell.viewWithTag(2) as! UIButton - increaseButton.showsTouchWhenHighlighted = true + //increaseButton.showsTouchWhenHighlighted = true increaseButton.addTarget(self, action: #selector(increaseGapLimit), for: .touchUpInside) return cell @@ -670,12 +795,39 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable configureCell(cell) let imageView = cell.viewWithTag(1) as! UIImageView - - imageView.image = exportWalletImage - let exportButton = cell.viewWithTag(2) as! UIButton configureExportButton(exportButton, indexPath: indexPath) + let chooseFormatButton = cell.viewWithTag(3) as! UIButton + configureChooseExportFormatButton(chooseFormatButton, indexPath: indexPath) + + let headerLabel = cell.viewWithTag(4) as! UILabel + let subheaderLabel = cell.viewWithTag(5) as! UILabel + + let enlargeButton = cell.viewWithTag(6) as! UIButton + configureEnlargeButton(enlargeButton, indexPath: indexPath) + + if urBytesFormat { + headerLabel.text = "UR Bytes" + subheaderLabel.text = "Passport, Keystone, Blue" + imageView.image = exportWalletImageURBytes + exportText = urBytes + } + + if bbqrFormat { + headerLabel.text = "BBQr" + subheaderLabel.text = "Coldcard" + imageView.image = exportWalletImageBBQr + exportText = bbqrText + } + + if outputDescFormat { + headerLabel.text = "UR Output Descriptor" + subheaderLabel.text = "Sparrow, Blue" + imageView.image = exportWalletImageCryptoOutput + exportText = outputDescUr + } + return cell } @@ -690,6 +842,15 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable let exportButton = cell.viewWithTag(2) as! UIButton configureExportButton(exportButton, indexPath: indexPath) + let headerLabel = cell.viewWithTag(4) as! UILabel + let subheaderLabel = cell.viewWithTag(5) as! UILabel + let chooseFormatButton = cell.viewWithTag(3) as! UIButton + let enlargeButton = cell.viewWithTag(6) as! UIButton + headerLabel.alpha = 0 + subheaderLabel.alpha = 0 + chooseFormatButton.alpha = 0 + enlargeButton.alpha = 0 + return cell } @@ -707,7 +868,7 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable } func numberOfSections(in tableView: UITableView) -> Int { - return 13 + return 14 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -716,10 +877,19 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable private func configureExportButton(_ button: UIButton, indexPath: IndexPath) { button.restorationIdentifier = "\(indexPath.section)" - button.showsTouchWhenHighlighted = true button.addTarget(self, action: #selector(exportButtonAction(_:)), for: .touchUpInside) } + private func configureChooseExportFormatButton(_ button: UIButton, indexPath: IndexPath) { + button.restorationIdentifier = "\(indexPath.section)" + button.addTarget(self, action: #selector(chooseExportFormatButtonAction(_:)), for: .touchUpInside) + } + + private func configureEnlargeButton(_ button: UIButton, indexPath: IndexPath) { + button.restorationIdentifier = "\(indexPath.section)" + button.addTarget(self, action: #selector(enlargeButtonAction(_:)), for: .touchUpInside) + } + private func configureCell(_ cell: UITableViewCell) { cell.selectionStyle = .none cell.layer.cornerRadius = 8 @@ -735,6 +905,8 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable originalLabel = wallet.label switch Section(rawValue: indexPath.section) { + case .backupText: + return backupTextCell(indexPath) case .label: return labelCell(indexPath) case .walletExport: @@ -766,10 +938,14 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { switch Section(rawValue: indexPath.section) { + case .backupText: + return 180 case .label: return 50 - case .backupQr, .walletExport: + case .backupQr: return 192 + case .walletExport: + return 270 case .exportFile: return 120 case .filename: @@ -810,7 +986,7 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable if let section = Section(rawValue: section) { switch section { case .walletExport: - textLabel.text = "This QR is for exporting your wallet to other Hardware Wallets and Software wallets. Compatible with Sparrow, Blue Wallet, Passport and more." + textLabel.text = "This QR is for exporting your wallet to other Hardware Wallets and Software wallets. Compatible with Sparrow, Blue Wallet, Passport, Coldcard and more." case .backupQr: textLabel.text = "This QR is best for restoring to Fully Noded, either QR works but this one includes the wallet label and blockheight your wallet was created at." @@ -1060,9 +1236,24 @@ class WalletDetailViewController: UIViewController, UITextFieldDelegate, UITable vc.headerIcon = UIImage(systemName: "rectangle.and.paperclip") vc.descriptionText = "Save this QR in lots of places so you can always easily recreate this wallet as watch-only. This QR code is best used with Fully Noded only." } else { - vc.headerText = "Wallet Export QR" - vc.headerIcon = UIImage(systemName: "square.and.arrow.up") - vc.descriptionText = "This QR code is best for exporting this wallet to other software and hardware wallets." + if bbqrFormat { + vc.headerText = "Wallet Export BBQr" + vc.headerIcon = UIImage(systemName: "square.and.arrow.up") + vc.descriptionText = "This QR code is best for exporting this wallet to Coldcard." + vc.isBbqr = true + } + + if outputDescFormat { + vc.headerText = "Wallet Export Descriptor" + vc.headerIcon = UIImage(systemName: "square.and.arrow.up") + vc.descriptionText = "This QR code is best for exporting this wallet to Sparrow, Passport, Blue Wallet and others..." + } + + if urBytesFormat { + vc.headerText = "Wallet Export UR Bytes" + vc.headerIcon = UIImage(systemName: "square.and.arrow.up") + vc.descriptionText = "This QR code is best for exporting this wallet to Passport, Blue Wallet and others..." + } } } default: @@ -1076,6 +1267,8 @@ extension WalletDetailViewController { private func headerName(for section: Section) -> (text: String, icon: UIImage, color: UIColor) { switch section { + case .backupText: + return ("Wallet Info", UIImage(systemName: "info.circle")!, .systemGray) case .label: return ("Label", UIImage(systemName: "rectangle.and.paperclip")!, .systemBlue) case .walletExport: