From df483e1002750180d0c7a7144130d0208e277bbd Mon Sep 17 00:00:00 2001 From: Russel Date: Thu, 29 Oct 2020 00:28:07 +0300 Subject: [PATCH 1/2] add qr code encoding/decoding --- .../FearlessUtils.xcodeproj/project.pbxproj | 20 +++ .../xcschemes/FearlessUtils-Example.xcscheme | 24 ++-- Example/Podfile | 6 + Example/Podfile.lock | 8 +- FearlessUtils.podspec | 6 +- .../Classes/Common/Data+AccountId.swift | 11 ++ .../Classes/QR/SubstrateQRCommon.swift | 42 +++++++ .../Classes/QR/SubstrateQRDecoder.swift | 48 ++++++++ .../Classes/QR/SubstrateQREncoder.swift | 28 +++++ QR/SubstrateQRCommon.swift | 9 ++ QR/SubstrateQRDecoder.swift | 8 ++ QR/SubstrateQREncoder.swift | 114 ++++++++++++++++++ Tests/QR/SubstrateQRDecoderTests.swift | 67 ++++++++++ Tests/QR/SubstrateQREncoderTests.swift | 33 +++++ 14 files changed, 404 insertions(+), 20 deletions(-) create mode 100644 FearlessUtils/Classes/Common/Data+AccountId.swift create mode 100644 FearlessUtils/Classes/QR/SubstrateQRCommon.swift create mode 100644 FearlessUtils/Classes/QR/SubstrateQRDecoder.swift create mode 100644 FearlessUtils/Classes/QR/SubstrateQREncoder.swift create mode 100644 QR/SubstrateQRCommon.swift create mode 100644 QR/SubstrateQRDecoder.swift create mode 100644 QR/SubstrateQREncoder.swift create mode 100644 Tests/QR/SubstrateQRDecoderTests.swift create mode 100644 Tests/QR/SubstrateQREncoderTests.swift diff --git a/Example/FearlessUtils.xcodeproj/project.pbxproj b/Example/FearlessUtils.xcodeproj/project.pbxproj index f0aa212..685ffb6 100644 --- a/Example/FearlessUtils.xcodeproj/project.pbxproj +++ b/Example/FearlessUtils.xcodeproj/project.pbxproj @@ -37,6 +37,8 @@ 84DA3B4324C8CE5A00B5E27F /* ScaleStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA3B3924C8CE5900B5E27F /* ScaleStringTests.swift */; }; 84DA3B4424C8CE5A00B5E27F /* ScaleInt16Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA3B3A24C8CE5900B5E27F /* ScaleInt16Tests.swift */; }; 84DA3B4524C8CE5A00B5E27F /* ScaleUInt8Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA3B3B24C8CE5900B5E27F /* ScaleUInt8Tests.swift */; }; + 84ED7256254A001A006102DF /* SubstrateQREncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED7255254A001A006102DF /* SubstrateQREncoderTests.swift */; }; + 84ED725C254A0233006102DF /* SubstrateQRDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED725B254A0233006102DF /* SubstrateQRDecoderTests.swift */; }; A0FDCFB5B61F738FEE34A5E6 /* Pods_FearlessUtils_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16D2E77D7010BEBF43DFE08E /* Pods_FearlessUtils_Tests.framework */; }; B2BF1BE136637C8404B79450 /* Pods_FearlessUtils_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9A63E44DBCDCE738F798358 /* Pods_FearlessUtils_Example.framework */; }; /* End PBXBuildFile section */ @@ -89,6 +91,8 @@ 84DA3B3A24C8CE5900B5E27F /* ScaleInt16Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScaleInt16Tests.swift; sourceTree = ""; }; 84DA3B3B24C8CE5900B5E27F /* ScaleUInt8Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScaleUInt8Tests.swift; sourceTree = ""; }; 84DA3B4824C8CEEF00B5E27F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = Tests/Info.plist; sourceTree = SOURCE_ROOT; }; + 84ED7255254A001A006102DF /* SubstrateQREncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateQREncoderTests.swift; sourceTree = ""; }; + 84ED725B254A0233006102DF /* SubstrateQRDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateQRDecoderTests.swift; sourceTree = ""; }; 93F24F5EEC204E4B0F918C39 /* Pods-FearlessUtils_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FearlessUtils_Example.debug.xcconfig"; path = "Target Support Files/Pods-FearlessUtils_Example/Pods-FearlessUtils_Example.debug.xcconfig"; sourceTree = ""; }; B82F3EE26AAEE44C6E7F9962 /* Pods-FearlessUtils_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FearlessUtils_Tests.release.xcconfig"; path = "Target Support Files/Pods-FearlessUtils_Tests/Pods-FearlessUtils_Tests.release.xcconfig"; sourceTree = ""; }; B9A63E44DBCDCE738F798358 /* Pods_FearlessUtils_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FearlessUtils_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -236,6 +240,7 @@ 84DA3B3024C8CE5900B5E27F /* Tests */ = { isa = PBXGroup; children = ( + 84ED7254254A0006006102DF /* QR */, 8467FD2324E71A0E005D486C /* Icon */, 84CB472224DBE81100837E11 /* Keystore */, 842D1E7424D0066100C30A7A /* Resources */, @@ -268,6 +273,15 @@ path = Scale; sourceTree = ""; }; + 84ED7254254A0006006102DF /* QR */ = { + isa = PBXGroup; + children = ( + 84ED7255254A001A006102DF /* SubstrateQREncoderTests.swift */, + 84ED725B254A0233006102DF /* SubstrateQRDecoderTests.swift */, + ); + path = QR; + sourceTree = ""; + }; A78EB2A0158FDFCA62F39CAC /* Pods */ = { isa = PBXGroup; children = ( @@ -499,6 +513,8 @@ 842D1E2324C98CEA00C30A7A /* StorageKeyFactoryTests.swift in Sources */, 8467FD2824E72CCD005D486C /* HSLColorTests.swift in Sources */, 842D1E2724C9B63300C30A7A /* ScaleOptionTests.swift in Sources */, + 84ED725C254A0233006102DF /* SubstrateQRDecoderTests.swift in Sources */, + 84ED7256254A001A006102DF /* SubstrateQREncoderTests.swift in Sources */, 842D1E7324CFF4EC00C30A7A /* KeypairDeriviationTests.swift in Sources */, 84CB472424DBE82C00837E11 /* KeystoreExtractorTests.swift in Sources */, 842D1E7124CFF31800C30A7A /* KeypairDeriviation.swift in Sources */, @@ -636,6 +652,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = YLWWUD25VZ; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; INFOPLIST_FILE = FearlessUtils/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; @@ -652,6 +669,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = YLWWUD25VZ; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; INFOPLIST_FILE = FearlessUtils/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; @@ -667,6 +685,7 @@ baseConfigurationReference = 7BCE5EB68B0A48835979E64A /* Pods-FearlessUtils_Tests.debug.xcconfig */; buildSettings = { DEVELOPMENT_TEAM = YLWWUD25VZ; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; FRAMEWORK_SEARCH_PATHS = ( "$(PLATFORM_DIR)/Developer/Library/Frameworks", "$(inherited)", @@ -690,6 +709,7 @@ baseConfigurationReference = B82F3EE26AAEE44C6E7F9962 /* Pods-FearlessUtils_Tests.release.xcconfig */; buildSettings = { DEVELOPMENT_TEAM = YLWWUD25VZ; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; FRAMEWORK_SEARCH_PATHS = ( "$(PLATFORM_DIR)/Developer/Library/Frameworks", "$(inherited)", diff --git a/Example/FearlessUtils.xcodeproj/xcshareddata/xcschemes/FearlessUtils-Example.xcscheme b/Example/FearlessUtils.xcodeproj/xcshareddata/xcschemes/FearlessUtils-Example.xcscheme index 31186ac..8c6d11b 100644 --- a/Example/FearlessUtils.xcodeproj/xcshareddata/xcschemes/FearlessUtils-Example.xcscheme +++ b/Example/FearlessUtils.xcodeproj/xcshareddata/xcschemes/FearlessUtils-Example.xcscheme @@ -40,8 +40,16 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -54,23 +62,11 @@ - - - - - - - - 5.0) - IrohaCrypto/ed25519 (~> 0.7.0) - IrohaCrypto/Scrypt (~> 0.7.0) @@ -44,12 +44,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BigInt: 74b4d88367b0e819d9f77393549226d36faeb0d8 - FearlessUtils: 8cec323272fe5dd811996b2c7c50587b857946d0 + FearlessUtils: d4e6e36e45ce2fb48c6981dba2192f1f6c7183eb IrohaCrypto: bddf15865c4df189b9c5273ba9c675afae8b7f4c scrypt.c: b42ae06183251329d2b2c620c226fb541a4a3592 TweetNacl: 3abf4d1d2082b0114e7a67410e300892448951e6 xxHash-Swift: 30bd6a7507b3b7348a277c49b1cb6346c2905ec7 -PODFILE CHECKSUM: 002fb4b06641c82f822cb8ddc9d08ba3f79755d6 +PODFILE CHECKSUM: 933d053287c20439760037c9b2fdaf8abafb515f -COCOAPODS: 1.9.3 +COCOAPODS: 1.10.0 diff --git a/FearlessUtils.podspec b/FearlessUtils.podspec index df949e3..81afb07 100644 --- a/FearlessUtils.podspec +++ b/FearlessUtils.podspec @@ -8,16 +8,20 @@ Pod::Spec.new do |s| s.name = 'FearlessUtils' - s.version = '0.7.0' + s.version = '0.8.0' s.summary = 'Utility library that implements clients specific logic to interact with substrate based networks' s.homepage = 'https://github.com/soramitsu/fearless-utils-iOS' s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'ERussel' => 'rezin@soramitsu.co.jp' } s.source = { :git => 'https://github.com/soramitsu/fearless-utils-iOS.git', :tag => s.version.to_s } + s.swift_version = '5.0' s.ios.deployment_target = '11.0' + s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64'} + s.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } + s.source_files = 'FearlessUtils/Classes/**/*' s.dependency 'IrohaCrypto/sr25519', '~> 0.7.0' s.dependency 'IrohaCrypto/ed25519', '~> 0.7.0' diff --git a/FearlessUtils/Classes/Common/Data+AccountId.swift b/FearlessUtils/Classes/Common/Data+AccountId.swift new file mode 100644 index 0000000..7da0881 --- /dev/null +++ b/FearlessUtils/Classes/Common/Data+AccountId.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Data { + func matchAccountId(_ accountId: Data) -> Bool { + if accountId == self { + return true + } + + return accountId == (try? self.blake2b32()) + } +} diff --git a/FearlessUtils/Classes/QR/SubstrateQRCommon.swift b/FearlessUtils/Classes/QR/SubstrateQRCommon.swift new file mode 100644 index 0000000..968eb57 --- /dev/null +++ b/FearlessUtils/Classes/QR/SubstrateQRCommon.swift @@ -0,0 +1,42 @@ +import Foundation + +public struct SubstrateQRInfo: Equatable { + public let prefix: String + public let address: String + public let rawPublicKey: Data + public let username: String? + + public init(prefix: String = SubstrateQR.prefix, + address: String, + rawPublicKey: Data, + username: String?) { + self.prefix = prefix + self.address = address + self.rawPublicKey = rawPublicKey + self.username = username + } +} + +public protocol SubstrateQREncodable { + func encode(info: SubstrateQRInfo) throws -> Data +} + +public protocol SubstrateQRDecodable { + func decode(data: Data) throws -> SubstrateQRInfo +} + +public enum SubstrateQREncoderError: Error, Equatable { + case brokenData +} + +public enum SubstrateQRDecoderError: Error, Equatable { + case brokenFormat + case unexpectedNumberOfFields + case undefinedPrefix + case accountIdMismatch +} + +public struct SubstrateQR { + public static let prefix: String = "substrate" + public static let fieldsSeparator: String = ":" +} diff --git a/FearlessUtils/Classes/QR/SubstrateQRDecoder.swift b/FearlessUtils/Classes/QR/SubstrateQRDecoder.swift new file mode 100644 index 0000000..8fcc572 --- /dev/null +++ b/FearlessUtils/Classes/QR/SubstrateQRDecoder.swift @@ -0,0 +1,48 @@ +import Foundation +import IrohaCrypto + +open class SubstrateQRDecoder: SubstrateQRDecodable { + public let networkType: SNAddressType + public let separator: String + public let prefix: String + + private lazy var addressFactory = SS58AddressFactory() + + public init(networkType: SNAddressType, + prefix: String = SubstrateQR.prefix, + separator: String = SubstrateQR.fieldsSeparator) { + self.prefix = prefix + self.networkType = networkType + self.separator = separator + } + + public func decode(data: Data) throws -> SubstrateQRInfo { + guard let fields = String(data: data, encoding: .utf8)? + .components(separatedBy: separator) else { + throw SubstrateQRDecoderError.brokenFormat + } + + guard fields.count >= 3, fields.count <= 4 else { + throw SubstrateQRDecoderError.unexpectedNumberOfFields + } + + guard fields[0] == prefix else { + throw SubstrateQRDecoderError.undefinedPrefix + } + + let address = fields[1] + let accountId = try addressFactory.accountId(fromAddress: address, type: networkType) + let publicKey = try Data(hexString: fields[2]) + + guard publicKey.matchAccountId(accountId) else { + throw SubstrateQRDecoderError.accountIdMismatch + } + + let username = fields.count > 3 ? fields[3] : nil + + return SubstrateQRInfo(prefix: prefix, + address: address, + rawPublicKey: publicKey, + username: username) + } +} diff --git a/FearlessUtils/Classes/QR/SubstrateQREncoder.swift b/FearlessUtils/Classes/QR/SubstrateQREncoder.swift new file mode 100644 index 0000000..e384e58 --- /dev/null +++ b/FearlessUtils/Classes/QR/SubstrateQREncoder.swift @@ -0,0 +1,28 @@ +import Foundation +import IrohaCrypto + +open class SubstrateQREncoder: SubstrateQREncodable { + let separator: String + + public init(separator: String = SubstrateQR.fieldsSeparator) { + self.separator = separator + } + + public func encode(info: SubstrateQRInfo) throws -> Data { + var fields: [String] = [ + info.prefix, + info.address, + info.rawPublicKey.toHex(includePrefix: true) + ] + + if let username = info.username { + fields.append(username) + } + + guard let data = fields.joined(separator: separator).data(using: .utf8) else { + throw SubstrateQREncoderError.brokenData + } + + return data + } +} diff --git a/QR/SubstrateQRCommon.swift b/QR/SubstrateQRCommon.swift new file mode 100644 index 0000000..4cad775 --- /dev/null +++ b/QR/SubstrateQRCommon.swift @@ -0,0 +1,9 @@ +import Foundation + +public protocol SubstrateQREncodable { + func encode(receiverInfo: ReceiveInfo) throws -> Data { +} + +public protocol SubstrateQRDecodable { + +} diff --git a/QR/SubstrateQRDecoder.swift b/QR/SubstrateQRDecoder.swift new file mode 100644 index 0000000..55fa14b --- /dev/null +++ b/QR/SubstrateQRDecoder.swift @@ -0,0 +1,8 @@ +// +// SubstrateQRDecoder.swift +// FearlessUtils +// +// Created by Ruslan Rezin on 28.10.2020. +// + +import Foundation diff --git a/QR/SubstrateQREncoder.swift b/QR/SubstrateQREncoder.swift new file mode 100644 index 0000000..dc676c1 --- /dev/null +++ b/QR/SubstrateQREncoder.swift @@ -0,0 +1,114 @@ +import Foundation + +enum WalletQREncoderError: Error { + case accountIdMismatch + case brokenData +} + +enum WalletQRDecoderError: Error { + case brokenFormat + case unexpectedNumberOfFields + case undefinedPrefix + case accountIdMismatch +} + +private extension Data { + func checkAccountId(_ accountId: Data) -> Bool { + if accountId == self { + return true + } + + return accountId == (try? self.blake2b32()) + } +} + +final class SubstrateQREncoder: WalletQREncoderProtocol { + let username: String? + let networkType: SNAddressType + let publicKey: Data + let prefix: String + let separator: String + + private lazy var addressFactory = SS58AddressFactory() + + init(networkType: SNAddressType, publicKey: Data, username: String?, prefix: String, separator: String) { + self.networkType = networkType + self.publicKey = publicKey + self.prefix = prefix + self.username = username + self.separator = separator + } + + func encode(receiverInfo: ReceiveInfo) throws -> Data { + let accountId = try Data(hexString: receiverInfo.accountId) + + guard publicKey.checkAccountId(accountId) else { + throw WalletQREncoderError.accountIdMismatch + } + + let address = try addressFactory.address(fromPublicKey: AccountIdWrapper(rawData: accountId), + type: networkType) + + var fields: [String] = [ + prefix, + address, + publicKey.toHex(includePrefix: true) + ] + + if let username = username { + fields.append(username) + } + + guard let data = fields.joined(separator: separator).data(using: .utf8) else { + throw WalletQREncoderError.brokenData + } + + return data + } +} + +final class WalletQRDecoder: WalletQRDecoderProtocol { + let networkType: SNAddressType + let prefix: String + let separator: String + + private lazy var addressFactory = SS58AddressFactory() + + init(networkType: SNAddressType, prefix: String, separator: String) { + self.networkType = networkType + self.prefix = prefix + self.separator = separator + } + + func decode(data: Data) throws -> ReceiveInfo { + guard let fields = String(data: data, encoding: .utf8)? + .components(separatedBy: separator) else { + throw WalletQRDecoderError.brokenFormat + } + + guard fields.count >= 3 else { + throw WalletQRDecoderError.unexpectedNumberOfFields + } + + guard fields[0] == prefix else { + throw WalletQRDecoderError.undefinedPrefix + } + + let accountId = try addressFactory.accountId(fromAddress: fields[1], type: networkType) + let publicKey = try Data(hexString: fields[2]) + + guard publicKey.checkAccountId(accountId) else { + throw WalletQRDecoderError.accountIdMismatch + } + + return ReceiveInfo(accountId: accountId.toHex(), + assetId: nil, + amount: nil, + details: nil) + } +} + +struct WalletQRCoderConstants { + static let prefix: String = "substrate" + static let fieldsSeparator: String = ":" +} diff --git a/Tests/QR/SubstrateQRDecoderTests.swift b/Tests/QR/SubstrateQRDecoderTests.swift new file mode 100644 index 0000000..02bbd87 --- /dev/null +++ b/Tests/QR/SubstrateQRDecoderTests.swift @@ -0,0 +1,67 @@ +import XCTest +import FearlessUtils + +class SubstrateQRDecoderTests: XCTestCase { + func testSuccessfullDecodingWithName() throws { + let data = try Data(hexString: "7375627374726174653a354633794b66354b7651343978356b346e5258624157674a485368516a51544245655a3950754e646d6562614e7837343a3078383432353834306636633337306637663264383239316333653639303436376530613866626238623036343866633964363439356439396463613763376133663a425642") + + let publicKey = try Data(hexString: "0x8425840f6c370f7f2d8291c3e690467e0a8fbb8b0648fc9d6495d99dca7c7a3f") + let expectedInfo = SubstrateQRInfo(prefix: SubstrateQR.prefix, + address: "5F3yKf5KvQ49x5k4nRXbAWgJHShQjQTBEeZ9PuNdmebaNx74", + rawPublicKey: publicKey, + username: "BVB") + + let decodedInfo = try SubstrateQRDecoder(networkType: .genericSubstrate).decode(data: data) + + XCTAssertEqual(decodedInfo, expectedInfo) + } + + func testSuccessfullDecodingWithoutName() throws { + let data = try Data(hexString: "7375627374726174653a354633794b66354b7651343978356b346e5258624157674a485368516a51544245655a3950754e646d6562614e7837343a307838343235383430663663333730663766326438323931633365363930343637653061386662623862303634386663396436343935643939646361376337613366") + + let publicKey = try Data(hexString: "0x8425840f6c370f7f2d8291c3e690467e0a8fbb8b0648fc9d6495d99dca7c7a3f") + let expectedInfo = SubstrateQRInfo(prefix: SubstrateQR.prefix, + address: "5F3yKf5KvQ49x5k4nRXbAWgJHShQjQTBEeZ9PuNdmebaNx74", + rawPublicKey: publicKey, + username: nil) + + let decodedInfo = try SubstrateQRDecoder(networkType: .genericSubstrate).decode(data: data) + + XCTAssertEqual(decodedInfo, expectedInfo) + } + + func testUndefinedPrefix() throws { + let data = try Data(hexString: "73756273747261743a354633794b66354b7651343978356b346e5258624157674a485368516a51544245655a3950754e646d6562614e7837343a307838343235383430663663333730663766326438323931633365363930343637653061386662623862303634386663396436343935643939646361376337613366") + + perforErrorTest(data, expectedError: .undefinedPrefix) + } + + func testUnexpectedNumberOfFields() throws { + let data = try Data(hexString: "7375627374726174653a354633794b66354b7651343978356b346e5258624157674a485368516a51544245655a3950754e646d6562614e7837343a3078383432353834306636633337306637663264383239316333653639303436376530613866626238623036343866633964363439356439396463613763376133663a31323a313233") + + perforErrorTest(data, expectedError: .unexpectedNumberOfFields) + } + + func testAccountIdMismatch() throws { + let data = try Data(hexString: "7375627374726174653a354633794b66354b7651343978356b346e5258624157674a485368516a51544245655a3950754e646d6562614e7837343a3078383432353834306636633337306637663364383239316333653639303436376530613866626238623036343866633964363439356439396463613763376133663a425642") + + perforErrorTest(data, expectedError: .accountIdMismatch) + } + + // MARK: Private + + private func perforErrorTest(_ data: Data, expectedError: SubstrateQRDecoderError) { + do { + _ = try SubstrateQRDecoder(networkType: .genericSubstrate).decode(data: data) + + XCTFail("Exception expected") + } catch { + guard let decodingError = error as? SubstrateQRDecoderError else { + XCTFail("Decoding error expected") + return + } + + XCTAssertEqual(decodingError, expectedError) + } + } +} diff --git a/Tests/QR/SubstrateQREncoderTests.swift b/Tests/QR/SubstrateQREncoderTests.swift new file mode 100644 index 0000000..a96f75a --- /dev/null +++ b/Tests/QR/SubstrateQREncoderTests.swift @@ -0,0 +1,33 @@ +import XCTest +import FearlessUtils + +class SubstrateQREncoderTests: XCTestCase { + + func testSuccessfullEncodingWithName() throws { + let expectedData = try Data(hexString: "7375627374726174653a354633794b66354b7651343978356b346e5258624157674a485368516a51544245655a3950754e646d6562614e7837343a3078383432353834306636633337306637663264383239316333653639303436376530613866626238623036343866633964363439356439396463613763376133663a425642") + + let publicKey = try Data(hexString: "0x8425840f6c370f7f2d8291c3e690467e0a8fbb8b0648fc9d6495d99dca7c7a3f") + let info = SubstrateQRInfo(prefix: SubstrateQR.prefix, + address: "5F3yKf5KvQ49x5k4nRXbAWgJHShQjQTBEeZ9PuNdmebaNx74", + rawPublicKey: publicKey, + username: "BVB") + + let resultData = try SubstrateQREncoder().encode(info: info) + + XCTAssertEqual(expectedData, resultData) + } + + func testSuccessfullEncodingWithoutName() throws { + let expectedData = try Data(hexString: "7375627374726174653a354633794b66354b7651343978356b346e5258624157674a485368516a51544245655a3950754e646d6562614e7837343a307838343235383430663663333730663766326438323931633365363930343637653061386662623862303634386663396436343935643939646361376337613366") + + let publicKey = try Data(hexString: "0x8425840f6c370f7f2d8291c3e690467e0a8fbb8b0648fc9d6495d99dca7c7a3f") + let info = SubstrateQRInfo(prefix: SubstrateQR.prefix, + address: "5F3yKf5KvQ49x5k4nRXbAWgJHShQjQTBEeZ9PuNdmebaNx74", + rawPublicKey: publicKey, + username: nil) + + let resultData = try SubstrateQREncoder().encode(info: info) + + XCTAssertEqual(expectedData, resultData) + } +} From 69091454d11d3814ffb63210b7dca40953e392ce Mon Sep 17 00:00:00 2001 From: Russel Date: Thu, 29 Oct 2020 16:46:50 +0300 Subject: [PATCH 2/2] fix old files --- QR/SubstrateQRCommon.swift | 9 --- QR/SubstrateQRDecoder.swift | 8 --- QR/SubstrateQREncoder.swift | 114 ------------------------------------ 3 files changed, 131 deletions(-) delete mode 100644 QR/SubstrateQRCommon.swift delete mode 100644 QR/SubstrateQRDecoder.swift delete mode 100644 QR/SubstrateQREncoder.swift diff --git a/QR/SubstrateQRCommon.swift b/QR/SubstrateQRCommon.swift deleted file mode 100644 index 4cad775..0000000 --- a/QR/SubstrateQRCommon.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -public protocol SubstrateQREncodable { - func encode(receiverInfo: ReceiveInfo) throws -> Data { -} - -public protocol SubstrateQRDecodable { - -} diff --git a/QR/SubstrateQRDecoder.swift b/QR/SubstrateQRDecoder.swift deleted file mode 100644 index 55fa14b..0000000 --- a/QR/SubstrateQRDecoder.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// SubstrateQRDecoder.swift -// FearlessUtils -// -// Created by Ruslan Rezin on 28.10.2020. -// - -import Foundation diff --git a/QR/SubstrateQREncoder.swift b/QR/SubstrateQREncoder.swift deleted file mode 100644 index dc676c1..0000000 --- a/QR/SubstrateQREncoder.swift +++ /dev/null @@ -1,114 +0,0 @@ -import Foundation - -enum WalletQREncoderError: Error { - case accountIdMismatch - case brokenData -} - -enum WalletQRDecoderError: Error { - case brokenFormat - case unexpectedNumberOfFields - case undefinedPrefix - case accountIdMismatch -} - -private extension Data { - func checkAccountId(_ accountId: Data) -> Bool { - if accountId == self { - return true - } - - return accountId == (try? self.blake2b32()) - } -} - -final class SubstrateQREncoder: WalletQREncoderProtocol { - let username: String? - let networkType: SNAddressType - let publicKey: Data - let prefix: String - let separator: String - - private lazy var addressFactory = SS58AddressFactory() - - init(networkType: SNAddressType, publicKey: Data, username: String?, prefix: String, separator: String) { - self.networkType = networkType - self.publicKey = publicKey - self.prefix = prefix - self.username = username - self.separator = separator - } - - func encode(receiverInfo: ReceiveInfo) throws -> Data { - let accountId = try Data(hexString: receiverInfo.accountId) - - guard publicKey.checkAccountId(accountId) else { - throw WalletQREncoderError.accountIdMismatch - } - - let address = try addressFactory.address(fromPublicKey: AccountIdWrapper(rawData: accountId), - type: networkType) - - var fields: [String] = [ - prefix, - address, - publicKey.toHex(includePrefix: true) - ] - - if let username = username { - fields.append(username) - } - - guard let data = fields.joined(separator: separator).data(using: .utf8) else { - throw WalletQREncoderError.brokenData - } - - return data - } -} - -final class WalletQRDecoder: WalletQRDecoderProtocol { - let networkType: SNAddressType - let prefix: String - let separator: String - - private lazy var addressFactory = SS58AddressFactory() - - init(networkType: SNAddressType, prefix: String, separator: String) { - self.networkType = networkType - self.prefix = prefix - self.separator = separator - } - - func decode(data: Data) throws -> ReceiveInfo { - guard let fields = String(data: data, encoding: .utf8)? - .components(separatedBy: separator) else { - throw WalletQRDecoderError.brokenFormat - } - - guard fields.count >= 3 else { - throw WalletQRDecoderError.unexpectedNumberOfFields - } - - guard fields[0] == prefix else { - throw WalletQRDecoderError.undefinedPrefix - } - - let accountId = try addressFactory.accountId(fromAddress: fields[1], type: networkType) - let publicKey = try Data(hexString: fields[2]) - - guard publicKey.checkAccountId(accountId) else { - throw WalletQRDecoderError.accountIdMismatch - } - - return ReceiveInfo(accountId: accountId.toHex(), - assetId: nil, - amount: nil, - details: nil) - } -} - -struct WalletQRCoderConstants { - static let prefix: String = "substrate" - static let fieldsSeparator: String = ":" -}