diff --git a/.gitignore b/.gitignore index f9d0fdc..aa926be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store .swiftpm +.build \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Android/wallet/src/androidTest/java/io/outblock/wallet/ExampleInstrumentedTest.kt b/Android/wallet/src/androidTest/java/io/outblock/wallet/ExampleInstrumentedTest.kt index 14c1219..4277671 100644 --- a/Android/wallet/src/androidTest/java/io/outblock/wallet/ExampleInstrumentedTest.kt +++ b/Android/wallet/src/androidTest/java/io/outblock/wallet/ExampleInstrumentedTest.kt @@ -61,4 +61,4 @@ class ExampleInstrumentedTest { .map { it.toInt(16).toByte() } .toByteArray() } -} \ No newline at end of file +} diff --git a/Android/wallet/src/androidTest/java/io/outblock/wallet/KeyManagerTest.kt b/Android/wallet/src/androidTest/java/io/outblock/wallet/KeyManagerTest.kt index 34a2df5..f4e578f 100644 --- a/Android/wallet/src/androidTest/java/io/outblock/wallet/KeyManagerTest.kt +++ b/Android/wallet/src/androidTest/java/io/outblock/wallet/KeyManagerTest.kt @@ -39,4 +39,4 @@ class KeyManagerTest { assertTrue(KeyManager.deleteEntry("test_prefix")) assertFalse(KeyManager.deleteEntry("nonexistent")) } -} \ No newline at end of file +} diff --git a/Android/wallet/src/test/java/io/outblock/wallet/ExampleUnitTest.kt b/Android/wallet/src/test/java/io/outblock/wallet/ExampleUnitTest.kt index fb0f922..86bb6a6 100644 --- a/Android/wallet/src/test/java/io/outblock/wallet/ExampleUnitTest.kt +++ b/Android/wallet/src/test/java/io/outblock/wallet/ExampleUnitTest.kt @@ -14,4 +14,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/Package.resolved b/Package.resolved index c1b36b3..80e6918 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,52 +1,59 @@ { - "object": { - "pins": [ - { - "package": "BigInt", - "repositoryURL": "https://github.com/attaswift/BigInt.git", - "state": { - "branch": null, - "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", - "version": "5.3.0" - } - }, - { - "package": "Flow", - "repositoryURL": "https://github.com/outblock/flow-swift.git", - "state": { - "branch": null, - "revision": "50b60826c4ce18adaa151946d11d468ac1fc98dd", - "version": "0.3.9" - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", - "state": { - "branch": null, - "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", - "version": "4.2.2" - } - }, - { - "package": "SwiftFormat", - "repositoryURL": "https://github.com/nicklockwood/SwiftFormat", - "state": { - "branch": null, - "revision": "fef156a6135e584985ed26713dd2e9ee41f952cb", - "version": "0.53.0" - } - }, - { - "package": "WalletCore", - "repositoryURL": "https://github.com/Outblock/wallet-core", - "state": { - "branch": "master", - "revision": "24497b1e6f34f171ad421dae8699758bda09e78d", - "version": null - } + "pins" : [ + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/attaswift/BigInt.git", + "state" : { + "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version" : "5.3.0" } - ] - }, - "version": 1 + }, + { + "identity" : "factory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hmlongco/Factory.git", + "state" : { + "revision" : "fb04a8918848e413d3921d346a23bae7f81088d9", + "version" : "2.4.3" + } + }, + { + "identity" : "flow-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/outblock/flow-swift.git", + "state" : { + "revision" : "50b60826c4ce18adaa151946d11d468ac1fc98dd", + "version" : "0.3.9" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat", + "state" : { + "revision" : "fef156a6135e584985ed26713dd2e9ee41f952cb", + "version" : "0.53.0" + } + }, + { + "identity" : "wallet-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Outblock/wallet-core", + "state" : { + "branch" : "master", + "revision" : "24497b1e6f34f171ad421dae8699758bda09e78d" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index deffae8..f80d49f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.5 +// swift-tools-version: 5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -17,8 +17,9 @@ let package = Package( dependencies: [ .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"), .package(url: "https://github.com/outblock/flow-swift.git", from: "0.3.9"), - .package(url: "https://github.com/Outblock/wallet-core", .branchItem("master")), + .package(url: "https://github.com/Outblock/wallet-core", branch: "master"), .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.4"), + .package(url: "https://github.com/hmlongco/Factory.git", from: "2.4.3") ], targets: [ .target( @@ -28,12 +29,15 @@ let package = Package( .product(name: "Flow", package: "flow-swift"), .product(name: "WalletCore", package: "wallet-core"), .product(name: "WalletCoreSwiftProtobuf", package: "wallet-core"), + .product(name: "Factory", package: "Factory") ], path: "iOS/FlowWalletKit/Sources" ), .testTarget( name: "FlowWalletKitTests", - dependencies: ["FlowWalletKit"], + dependencies: [ + "FlowWalletKit" + ], path: "iOS/FlowWalletKit/Tests" ), ] diff --git a/iOS/FlowWalletKit/Sources/Crypto/BIP39.swift b/iOS/FlowWalletKit/Sources/Crypto/BIP39.swift index befeb32..0808509 100644 --- a/iOS/FlowWalletKit/Sources/Crypto/BIP39.swift +++ b/iOS/FlowWalletKit/Sources/Crypto/BIP39.swift @@ -9,7 +9,7 @@ import Foundation import WalletCore public enum BIP39 { - public enum SeedPhraseLength: Int,Codable { + public enum SeedPhraseLength: Int, Codable, Sendable { case twelve = 12 case fifteen = 15 case twentyFour = 24 diff --git a/iOS/FlowWalletKit/Sources/DI/Container.swift b/iOS/FlowWalletKit/Sources/DI/Container.swift new file mode 100644 index 0000000..9ba122a --- /dev/null +++ b/iOS/FlowWalletKit/Sources/DI/Container.swift @@ -0,0 +1,15 @@ +// +// Container.swift +// FlowWalletKit +// +// Created by Marty Ulrich on 3/24/25. +// + +import Factory +import Foundation + +extension Container { + var keychainStorage: Factory { + self { KeychainStorage(service: Bundle.main.bundleIdentifier!, label: "keychain", synchronizable: false) } + } +} diff --git a/iOS/FlowWalletKit/Sources/FWKManager.swift b/iOS/FlowWalletKit/Sources/FWKManager.swift deleted file mode 100644 index 3fd932d..0000000 --- a/iOS/FlowWalletKit/Sources/FWKManager.swift +++ /dev/null @@ -1,30 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book - -import Flow -import KeychainAccess -import WalletCore - -public class FWKManager { - static let shared = FWKManager() - private static var config: Config? - - public let storage: any StorageProtocol - - public class func setup(_ config: Config) { - FWKManager.config = config - } - - private init() { - guard let config = FWKManager.config else { - fatalError("Error - you must call setup before accessing FlowWalletKit.shared") - } - storage = config.storage - } -} - -public extension FWKManager { - struct Config { - let storage: any StorageProtocol - } -} diff --git a/iOS/FlowWalletKit/Sources/Keys/KeyProtocol.swift b/iOS/FlowWalletKit/Sources/Keys/KeyProtocol.swift index 11771d8..963deb4 100644 --- a/iOS/FlowWalletKit/Sources/Keys/KeyProtocol.swift +++ b/iOS/FlowWalletKit/Sources/Keys/KeyProtocol.swift @@ -23,13 +23,13 @@ public protocol KeyProtocol: Identifiable { var keyType: KeyType { get } - var storage: StorageProtocol { set get } + var storage: StorageProtocol { get } - static func create(_ advance: Advance, storage: StorageProtocol) throws -> Key - static func create(storage: StorageProtocol) throws -> Key - static func createAndStore(id: String, password: String, storage: StorageProtocol) throws -> Key - static func get(id: String, password: String, storage: StorageProtocol) throws -> Key - static func restore(secret: Secret, storage: StorageProtocol) throws -> Key + static func create(_ advance: Advance) throws -> Key + static func create() throws -> Key + static func createAndStore(id: String, password: String) throws -> Key + static func get(id: String, password: String) throws -> Key + static func restore(secret: Secret) throws -> Key func store(id: String, password: String) throws func isValidSignature(signature: Data, message: Data, signAlgo: Flow.SignatureAlgorithm) -> Bool @@ -42,10 +42,6 @@ public protocol KeyProtocol: Identifiable { } public extension KeyProtocol { - var storage: StorageProtocol { - FWKManager.shared.storage - } - var id: String { if let data = publicKey(signAlgo: .ECDSA_P256) { return data.hexString @@ -66,7 +62,7 @@ public extension KeyProtocol { storage.allKeys } - static func create(_: Advance, storage _: any StorageProtocol) throws -> Key { + static func create(_: Advance) throws -> Key { throw WalletError.noImplement } } diff --git a/iOS/FlowWalletKit/Sources/Keys/PrivateKey.swift b/iOS/FlowWalletKit/Sources/Keys/PrivateKey.swift index e9a6adc..aef7cd8 100644 --- a/iOS/FlowWalletKit/Sources/Keys/PrivateKey.swift +++ b/iOS/FlowWalletKit/Sources/Keys/PrivateKey.swift @@ -10,57 +10,58 @@ import Flow import Foundation import KeychainAccess import WalletCore - -public class PrivateKey: KeyProtocol { - public typealias Advance = String - - public var storage: any StorageProtocol - public var keyType: KeyType = .privateKey - public let pk: WalletCore.PrivateKey +import Factory + +class PrivateKey: KeyProtocol { + typealias Key = PrivateKey + + typealias Secret = Data + + typealias Advance = String + + @Injected(\.keychainStorage) var storage + var keyType: KeyType = .privateKey + let pk: WalletCore.PrivateKey init() { pk = WalletCore.PrivateKey() - storage = FWKManager.shared.storage } - init(storage: any StorageProtocol) { - pk = WalletCore.PrivateKey() - self.storage = storage - } - - init(pk: WalletCore.PrivateKey, storage: any StorageProtocol) { + init(pk: WalletCore.PrivateKey) { self.pk = pk - self.storage = storage } - public static func create(storage: any StorageProtocol) throws -> PrivateKey { + static func create() throws -> PrivateKey { let pk = WalletCore.PrivateKey() - return PrivateKey(pk: pk, storage: storage) + return PrivateKey(pk: pk) } - public func create(id: String, password: String) throws -> PrivateKey { + static func create(id: String, password: String) throws -> PrivateKey { let pk = WalletCore.PrivateKey() guard let cipher = ChaChaPolyCipher(key: password) else { throw WalletError.initChaChapolyFailed } let encrypted = try cipher.encrypt(data: pk.data) + @Injected(\.keychainStorage) var storage try storage.set(id, value: encrypted) - return PrivateKey(pk: pk, storage: storage) + return PrivateKey(pk: pk) } - public static func createAndStore(id: String, password: String, storage: any StorageProtocol) throws -> PrivateKey { + static func createAndStore(id: String, password: String) throws -> PrivateKey { let pk = WalletCore.PrivateKey() guard let cipher = ChaChaPolyCipher(key: password) else { throw WalletError.initChaChapolyFailed } let encrypted = try cipher.encrypt(data: pk.data) + @Injected(\.keychainStorage) var storage try storage.set(id, value: encrypted) - return PrivateKey(pk: pk, storage: storage) + return PrivateKey(pk: pk) } - public static func get(id: String, password: String, storage: any StorageProtocol) throws -> PrivateKey { + static func get(id: String, password: String) throws -> PrivateKey { + @Injected(\.keychainStorage) var storage guard let data = try storage.get(id) else { throw WalletError.emptyKeychain } @@ -75,17 +76,17 @@ public class PrivateKey: KeyProtocol { throw WalletError.initPrivateKeyFailed } - return PrivateKey(pk: pk, storage: storage) + return PrivateKey(pk: pk) } - public static func restore(secret: Data, storage: any StorageProtocol) throws -> PrivateKey { + static func restore(secret: Data) throws -> PrivateKey { guard let pk = WalletCore.PrivateKey(data: secret) else { throw WalletError.restoreWalletFailed } - return PrivateKey(pk: pk, storage: storage) + return PrivateKey(pk: pk) } - public static func restore(json: String, password: String, storage: any StorageProtocol) throws -> PrivateKey { + func restore(json: String, password: String) throws -> PrivateKey { guard let jsonData = json.data(using: .utf8), let passwordData = password.data(using: .utf8) else { throw WalletError.restoreWalletFailed } @@ -101,10 +102,10 @@ public class PrivateKey: KeyProtocol { throw WalletError.invaildPrivateKey } - return PrivateKey(pk: pk, storage: storage) + return PrivateKey(pk: pk) } - public func store(id: String, password: String) throws { + func store(id: String, password: String) throws { guard let cipher = ChaChaPolyCipher(key: password) else { throw WalletError.initChaChapolyFailed } @@ -113,25 +114,25 @@ public class PrivateKey: KeyProtocol { try storage.set(id, value: encrypted) } - public func isValidSignature(signature: Data, message: Data, signAlgo: Flow.SignatureAlgorithm) -> Bool { + func isValidSignature(signature: Data, message: Data, signAlgo: Flow.SignatureAlgorithm) -> Bool { guard let pubK = try? getPublicKey(signAlgo: signAlgo) else { return false } return pubK.verify(signature: signature, message: message) } - public func publicKey(signAlgo: Flow.SignatureAlgorithm) -> Data? { + func publicKey(signAlgo: Flow.SignatureAlgorithm) -> Data? { guard let pubK = try? getPublicKey(signAlgo: signAlgo) else { return nil } return pubK.uncompressed.data.dropFirst() } - public func privateKey(signAlgo: Flow.SignatureAlgorithm = .ECDSA_P256) -> Data? { + func privateKey(signAlgo: Flow.SignatureAlgorithm = .ECDSA_P256) -> Data? { return pk.data } - public func sign(data: Data, signAlgo: Flow.SignatureAlgorithm, hashAlgo: Flow.HashAlgorithm) throws -> Data { + func sign(data: Data, signAlgo: Flow.SignatureAlgorithm, hashAlgo: Flow.HashAlgorithm) throws -> Data { let hashed = try hashAlgo.hash(data: data) guard let curve = signAlgo.WCCurve else { throw WalletError.unsupportSignatureAlgorithm @@ -153,6 +154,4 @@ public class PrivateKey: KeyProtocol { throw WalletError.unsupportSignatureAlgorithm } } - - } diff --git a/iOS/FlowWalletKit/Sources/Keys/SecureEnclaveKey.swift b/iOS/FlowWalletKit/Sources/Keys/SecureEnclaveKey.swift index b569ca3..07c7ad0 100644 --- a/iOS/FlowWalletKit/Sources/Keys/SecureEnclaveKey.swift +++ b/iOS/FlowWalletKit/Sources/Keys/SecureEnclaveKey.swift @@ -10,35 +10,37 @@ import Flow import Foundation import KeychainAccess import WalletCore +import Factory public class SecureEnclaveKey: KeyProtocol { public typealias Advance = String public var keyType: KeyType = .secureEnclave public let key: SecureEnclave.P256.Signing.PrivateKey - public var storage: any StorageProtocol + @Injected(\.keychainStorage) public var storage - public init(key: SecureEnclave.P256.Signing.PrivateKey, storage: any StorageProtocol) { + public init(key: SecureEnclave.P256.Signing.PrivateKey) { self.key = key - self.storage = storage } - public static func create(storage: any StorageProtocol) throws -> SecureEnclaveKey { + public static func create() throws -> SecureEnclaveKey { let key = try SecureEnclave.P256.Signing.PrivateKey() - return SecureEnclaveKey(key: key, storage: storage) + return SecureEnclaveKey(key: key) } - public static func createAndStore(id: String, password: String, storage: any StorageProtocol) throws -> SecureEnclaveKey { + public static func createAndStore(id: String, password: String) throws -> SecureEnclaveKey { guard let cipher = ChaChaPolyCipher(key: password) else { throw WalletError.initChaChapolyFailed } let key = try SecureEnclave.P256.Signing.PrivateKey() let encrypted = try cipher.encrypt(data: key.dataRepresentation) + @Injected(\.keychainStorage) var storage try storage.set(id, value: encrypted) - return SecureEnclaveKey(key: key, storage: storage) + return SecureEnclaveKey(key: key) } - public static func get(id: String, password: String, storage: any StorageProtocol) throws -> SecureEnclaveKey { + public static func get(id: String, password: String) throws -> SecureEnclaveKey { + @Injected(\.keychainStorage) var storage guard let data = try storage.get(id) else { throw WalletError.emptyKeychain } @@ -49,12 +51,12 @@ public class SecureEnclaveKey: KeyProtocol { let pk = try cipher.decrypt(combinedData: data) let key = try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: pk) - return SecureEnclaveKey(key: key, storage: storage) + return SecureEnclaveKey(key: key) } - public static func restore(secret: Data, storage: any StorageProtocol) throws -> SecureEnclaveKey { + public static func restore(secret: Data) throws -> SecureEnclaveKey { let key = try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: secret) - return SecureEnclaveKey(key: key, storage: storage) + return SecureEnclaveKey(key: key) } public func store(id: String, password: String) throws { @@ -90,7 +92,7 @@ public class SecureEnclaveKey: KeyProtocol { signAlgo _: Flow.SignatureAlgorithm = .ECDSA_P256, hashAlgo: Flow.HashAlgorithm) throws -> Data { - let hashed = SHA256.hash(data: data) + let hashed = SHA256.hash(data: data) return try key.signature(for: hashed).rawRepresentation } diff --git a/iOS/FlowWalletKit/Sources/Keys/SeedPhraseKey.swift b/iOS/FlowWalletKit/Sources/Keys/SeedPhraseKey.swift index 2127c6d..2e5b873 100644 --- a/iOS/FlowWalletKit/Sources/Keys/SeedPhraseKey.swift +++ b/iOS/FlowWalletKit/Sources/Keys/SeedPhraseKey.swift @@ -10,6 +10,9 @@ import Flow import Foundation import KeychainAccess import WalletCore +import Factory + +public let defaultSeedPhraseLength: BIP39.SeedPhraseLength = .twelve public class SeedPhraseKey: KeyProtocol { public struct AdvanceOption { @@ -25,37 +28,32 @@ public class SeedPhraseKey: KeyProtocol { let passphrase: String } - public var storage: any StorageProtocol - public var keyType: KeyType = .seedPhrase - public var derivationPath = "m/44'/539'/0'/0/0" - public var passphrase: String = "" - public var seedPhraseLength: BIP39.SeedPhraseLength = SeedPhraseKey.defaultSeedPhraseLength - - public static let defaultSeedPhraseLength: BIP39.SeedPhraseLength = .twelve + @Injected(\.keychainStorage) public var storage + public let keyType: KeyType = .seedPhrase + public let derivationPath: String + public let passphrase: String + public let seedPhraseLength: BIP39.SeedPhraseLength public let hdWallet: HDWallet - - public init(hdWallet: HDWallet, - storage: any StorageProtocol, - derivationPath: String = "m/44'/539'/0'/0/0", - passphrase: String = "", - seedPhraseLength: BIP39.SeedPhraseLength = SeedPhraseKey.defaultSeedPhraseLength - ) - { + + public init( + hdWallet: HDWallet, + derivationPath: String = "m/44'/539'/0'/0/0", + passphrase: String = "", + seedPhraseLength: BIP39.SeedPhraseLength = defaultSeedPhraseLength + ) { self.hdWallet = hdWallet - self.storage = storage self.derivationPath = derivationPath self.passphrase = passphrase self.seedPhraseLength = seedPhraseLength } - public static func create(_ advance: AdvanceOption, storage: any StorageProtocol) throws -> SeedPhraseKey { + static public func create(_ advance: AdvanceOption) throws -> SeedPhraseKey { guard let hdWallet = HDWallet(strength: advance.seedPhraseLength.strength, passphrase: advance.passphrase) else { throw WalletError.initHDWalletFailed } let key = SeedPhraseKey(hdWallet: hdWallet, - storage: storage, derivationPath: advance.derivationPath, passphrase: advance.passphrase, seedPhraseLength: advance.seedPhraseLength @@ -63,15 +61,15 @@ public class SeedPhraseKey: KeyProtocol { return key } - public static func create(storage: any StorageProtocol) throws -> SeedPhraseKey { - guard let hdWallet = HDWallet(strength: SeedPhraseKey.defaultSeedPhraseLength.strength, passphrase: "") else { + static public func create() throws -> SeedPhraseKey { + guard let hdWallet = HDWallet(strength: defaultSeedPhraseLength.strength, passphrase: "") else { throw WalletError.initHDWalletFailed } - return SeedPhraseKey(hdWallet: hdWallet, storage: storage) + return SeedPhraseKey(hdWallet: hdWallet) } - public static func createAndStore(id: String, password: String, storage: any StorageProtocol) throws -> SeedPhraseKey { - guard let hdWallet = HDWallet(strength: SeedPhraseKey.defaultSeedPhraseLength.strength, passphrase: "") else { + static public func createAndStore(id: String, password: String) throws -> SeedPhraseKey { + guard let hdWallet = HDWallet(strength: defaultSeedPhraseLength.strength, passphrase: "") else { throw WalletError.initHDWalletFailed } @@ -79,13 +77,13 @@ public class SeedPhraseKey: KeyProtocol { throw WalletError.initChaChapolyFailed } - let encrypted = try cipher.encrypt(data: hdWallet.entropy) - let key = SeedPhraseKey(hdWallet: hdWallet, storage: storage) + let key = SeedPhraseKey(hdWallet: hdWallet) try key.store(id: id, password: password) return key } - public static func get(id: String, password: String, storage: any StorageProtocol) throws -> SeedPhraseKey { + static public func get(id: String, password: String) throws -> SeedPhraseKey { + @Injected(\.keychainStorage) var storage guard let data = try storage.get(id) else { throw WalletError.emptyKeychain } @@ -101,7 +99,7 @@ public class SeedPhraseKey: KeyProtocol { throw WalletError.initHDWalletFailed } - return SeedPhraseKey(hdWallet: hdWallet, storage: storage, derivationPath: model.derivationPath, passphrase: model.passphrase, seedPhraseLength: model.seedPhraseLength) + return SeedPhraseKey(hdWallet: hdWallet, derivationPath: model.derivationPath, passphrase: model.passphrase, seedPhraseLength: model.seedPhraseLength) } public func isValidSignature(signature: Data, message: Data, signAlgo: Flow.SignatureAlgorithm) -> Bool { @@ -121,13 +119,12 @@ public class SeedPhraseKey: KeyProtocol { try storage.set(id, value: encrypted) } - public static func restore(secret: KeyData, storage: any StorageProtocol) throws -> SeedPhraseKey { + static public func restore(secret: KeyData) throws -> SeedPhraseKey { guard let wallet = HDWallet(mnemonic: secret.mnemonic, passphrase: secret.passphrase) else { throw WalletError.restoreWalletFailed } - let key = SeedPhraseKey(hdWallet: wallet, storage: storage, - derivationPath: secret.derivationPath, passphrase: secret.passphrase) + let key = SeedPhraseKey(hdWallet: wallet, derivationPath: secret.derivationPath, passphrase: secret.passphrase) return key } diff --git a/iOS/FlowWalletKit/Sources/Storage/KeychainStorage.swift b/iOS/FlowWalletKit/Sources/Storage/KeychainStorage.swift index d2c7976..8dfe829 100644 --- a/iOS/FlowWalletKit/Sources/Storage/KeychainStorage.swift +++ b/iOS/FlowWalletKit/Sources/Storage/KeychainStorage.swift @@ -8,12 +8,14 @@ import Foundation import KeychainAccess +//TODO: should this be an actor? +//TODO: don't make implementations public, have public factories that make them and return protocol. StorageProtocol prob doesn't need to be public though. public class KeychainStorage: StorageProtocol { let service: String let label: String let synchronizable: Bool let accessGroup: String? - var keychain: Keychain + let keychain: Keychain public init( service: String, diff --git a/iOS/FlowWalletKit/Sources/Storage/StorageProtocol.swift b/iOS/FlowWalletKit/Sources/Storage/StorageProtocol.swift index 8a810d7..b36ef7c 100644 --- a/iOS/FlowWalletKit/Sources/Storage/StorageProtocol.swift +++ b/iOS/FlowWalletKit/Sources/Storage/StorageProtocol.swift @@ -7,10 +7,8 @@ import Foundation +//TODO: these should be async. might want to add a cache if we fetch from keychain often. should be internal? public protocol StorageProtocol { -// associatedtype Key -// associatedtype Value - var allKeys: [String] { get } func findKey(_ keyword: String) throws -> [String] func get(_ key: String) throws -> Data? @@ -18,3 +16,11 @@ public protocol StorageProtocol { func remove(_ key: String) throws func removeAll() throws } + +extension StorageProtocol { + public func findKey(_ keyword: String) throws -> [String] { + allKeys.filter { key in + key.contains(keyword) + } + } +} diff --git a/iOS/FlowWalletKit/Sources/Wallet/Wallet.swift b/iOS/FlowWalletKit/Sources/Wallet/Wallet.swift index e5c2a8f..abcbbed 100644 --- a/iOS/FlowWalletKit/Sources/Wallet/Wallet.swift +++ b/iOS/FlowWalletKit/Sources/Wallet/Wallet.swift @@ -5,7 +5,8 @@ // Created by Hao Fu on 12/9/2024. // -import Flow +//TODO: REMOVE @preconcurrency +@preconcurrency import Flow import Foundation public enum WalletType { @@ -59,18 +60,22 @@ public class Wallet: ObservableObject { public init(type: WalletType, networks: Set = [.mainnet, .testnet]) { self.type = type self.networks = networks - fetchAccount() + Task { + await fetchAccount() + } } - public func fetchAccount() { - Task { + public func fetchAccount() async { + do { + try loadCahe() do { - try loadCahe() - try await _ = fetchAllNetworkAccounts() + _ = try await fetchAllNetworkAccounts() try cache() } catch { // TODO: Handle Error } + } catch { + // TODO: Handle Error } } @@ -97,20 +102,20 @@ public class Wallet: ObservableObject { public func account(chainID: Flow.ChainID) async throws -> [Flow.Account] { guard case let .key(key) = type else { if case let .watch(address) = type { - return [try await flow.getAccountAtLatestBlock(address: address)] + return [try await Flow.shared.accessAPI.getAccountAtLatestBlock(address: address)] } throw WalletError.invaildWalletType } var accounts: [Flow.Account] = [] if let p256Key = try key.publicKey(signAlgo: .ECDSA_P256)?.hexString { - async let p256KeyRequest = Network.findFlowAccountByKey(publicKey: p256Key, chainID: chainID) - try await accounts += p256KeyRequest + let p256KeyRequest = try await Network.findFlowAccountByKey(publicKey: p256Key, chainID: chainID) + accounts.append(contentsOf: p256KeyRequest) } if let secp256k1Key = try key.publicKey(signAlgo: .ECDSA_SECP256k1)?.hexString { - async let secp256k1KeyRequest = Network.findFlowAccountByKey(publicKey: secp256k1Key, chainID: chainID) - try await accounts += secp256k1KeyRequest + let secp256k1KeyRequest = try await Network.findFlowAccountByKey(publicKey: secp256k1Key, chainID: chainID) + accounts.append(contentsOf: secp256k1KeyRequest) } return accounts @@ -119,20 +124,20 @@ public class Wallet: ObservableObject { public func fullAccount(chainID: Flow.ChainID) async throws -> [Flow.Account] { guard case let .key(key) = type else { if case let .watch(address) = type { - return [try await flow.getAccountAtLatestBlock(address: address)] + return [try await Flow.shared.accessAPI.getAccountAtLatestBlock(address: address)] } throw WalletError.invaildWalletType } var accounts: [KeyIndexerResponse.Account] = [] if let p256Key = try key.publicKey(signAlgo: .ECDSA_P256)?.hexString { - async let p256KeyRequest = Network.findAccountByKey(publicKey: p256Key, chainID: chainID) - try await accounts += p256KeyRequest + let p256KeyRequest = try await Network.findAccountByKey(publicKey: p256Key, chainID: chainID) + accounts.append(contentsOf: p256KeyRequest) } if let secp256k1Key = try key.publicKey(signAlgo: .ECDSA_SECP256k1)?.hexString { - async let secp256k1KeyRequest = Network.findAccountByKey(publicKey: secp256k1Key, chainID: chainID) - try await accounts += secp256k1KeyRequest + let secp256k1KeyRequest = try await Network.findAccountByKey(publicKey: secp256k1Key, chainID: chainID) + accounts.append(contentsOf: secp256k1KeyRequest) } let addresses = Set(accounts).compactMap { Flow.Address(hex: $0.address) } @@ -159,20 +164,25 @@ public class Wallet: ObservableObject { // MARK: - Cache public func cache() throws { - // TODO: Handle other type - guard let flowAccounts, case let .key(key) = type else { + guard let flowAccounts else { return } let data = try JSONEncoder().encode(flowAccounts) - try key.storage.set(cacheId, value: data) + if case let .key(key) = type { + try key.storage.set(cacheId, value: data) + } } public func loadCahe() throws { - // TODO: Handle other type - guard case let .key(key) = type, let data = try key.storage.get(cacheId) else { - throw WalletError.loadCacheFailed + guard case let .key(key) = type else { + return + } + + guard let data = try key.storage.get(cacheId) else { + return } + let model = try JSONDecoder().decode([Flow.ChainID: [Flow.Account]].self, from: data) flowAccounts = model diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKit.xctestplan b/iOS/FlowWalletKit/Tests/FlowWalletKit.xctestplan new file mode 100644 index 0000000..4084ef6 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKit.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "D498CDDB-ECB2-49F1-AB5D-00280B37AB26", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "FlowWalletKitTests", + "name" : "FlowWalletKitTests" + } + } + ], + "version" : 1 +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Crypto/BIP39Tests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Crypto/BIP39Tests.swift new file mode 100644 index 0000000..5bd03f5 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Crypto/BIP39Tests.swift @@ -0,0 +1,70 @@ +import Foundation +import Testing +@testable import FlowWalletKit + +struct BIP39Tests { + + @Test + func testSeedPhraseGeneration() { + // Given + let entropy = Data(repeating: 1, count: 16) // 128 bits of entropy + + // When + let mnemonic = BIP39.generate(passphrase: String(data: entropy, encoding: .utf8)!) + + // Then + #expect(mnemonic != nil) + if let phrase = mnemonic { + let words = phrase.split(separator: " ") + #expect(words.count == 12) // 128 bits of entropy should produce 12 words + } + } + + @Test + func testSeedFromMnemonic() { + // Given + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + // When + let seed = BIP39.generate(.twelve, passphrase: mnemonic) + + // Then + #expect(seed != nil) + if let seedData = seed { + #expect(seedData.count == 64) // Seed should be 512 bits (64 bytes) + + // Compare with known test vector result (first 16 bytes for verification) + let expectedHexStart = "c55257c360c07c72029aebc1b53c05ed" + let actualHexStart = seedData.prefix(16).lowercased() + #expect(actualHexStart == expectedHexStart) + } + } + + @Test + func testInvalidMnemonic() { + // Given + let invalidMnemonic = "invalid words that are not in the bip39 wordlist" + + // When + let isValid = BIP39.isValid(mnemonic: invalidMnemonic) + + // Then + #expect(isValid == false) + } + + @Test + func testDifferentPassphraseProducesDifferentSeed() { + // Given + let passphrase1 = "passphrase1" + let passphrase2 = "passphrase2" + + // When + let seed1 = BIP39.generate(passphrase: passphrase1) + let seed2 = BIP39.generate(passphrase: passphrase2) + + // Then + #expect(seed1 != nil) + #expect(seed2 != nil) + #expect(seed1 != seed2) + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Crypto/SymmetricEncryptionTests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Crypto/SymmetricEncryptionTests.swift new file mode 100644 index 0000000..a99bf24 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Crypto/SymmetricEncryptionTests.swift @@ -0,0 +1,66 @@ +import Foundation +import Testing +@testable import FlowWalletKit + +struct SymmetricEncryptionTests { + + @Test + func testChaChaPolyEncryptDecrypt() throws { + // Given + let password = "TestPassword123" + + guard let cipher = ChaChaPolyCipher(key: password) else { + #expect(false, "Failed to initialize ChaChaPolyCipher") + return + } + + let originalData = "Secret information to encrypt".data(using: .utf8)! + + // When + let encryptedData = try cipher.encrypt(data: originalData) + let decryptedData = try cipher.decrypt(combinedData: encryptedData) + + // Then + #expect(decryptedData == originalData) + } + + @Test + func testEncryptionWithDifferentPasswords() throws { + // Given + let password1 = "Password1" + let password2 = "Password2" + + guard let cipher1 = ChaChaPolyCipher(key: password1), + let cipher2 = ChaChaPolyCipher(key: password2) else { + #expect(false, "Failed to initialize ChaChaPolyCipher") + return + } + + let originalData = "Secret information to encrypt".data(using: .utf8)! + + // When + let encryptedData = try cipher1.encrypt(data: originalData) + + // Then - Attempting to decrypt with wrong password should fail + #expect(throws: Error.self) { + try cipher2.decrypt(combinedData: encryptedData) + } + } + + @Test + func testEncryptionWithInvalidData() throws { + // Given + let password = "TestPassword123" + guard let cipher = ChaChaPolyCipher(key: password) else { + #expect(false, "Failed to initialize ChaChaPolyCipher") + return + } + + let invalidData = Data(repeating: 0, count: 10) // Too small to be valid combined data + + // Then - Attempting to decrypt invalid data should fail + #expect(throws: Error.self) { + try cipher.decrypt(combinedData: invalidData) + } + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/ExtensionTests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/ExtensionTests.swift new file mode 100644 index 0000000..6b72e29 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/ExtensionTests.swift @@ -0,0 +1,77 @@ +import Foundation +import Testing +import Flow +@testable import FlowWalletKit + +struct ExtensionTests { + + @Test + func testHashAlgorithmExtension() throws { + // Given + let testData = "test data".data(using: .utf8)! + + // When + let sha2Result = try Flow.HashAlgorithm.SHA2_256.hash(data: testData) + let sha3Result = try Flow.HashAlgorithm.SHA3_256.hash(data: testData) + + // Then + #expect(sha2Result.count == 32) // SHA256 produces 32 bytes + #expect(sha3Result.count == 32) // SHA3_256 produces 32 bytes + #expect(sha2Result != sha3Result) // Different algorithms should produce different results + } + + @Test + func testUnsupportedHashAlgorithm() { + // Given + let testData = "test data".data(using: .utf8)! + let unsupportedAlgo = Flow.HashAlgorithm.unknown + + // Then + #expect(throws:WalletError.unsupportHashAlgorithm) { + try unsupportedAlgo.hash(data: testData) + } + } + +// func testSignatureAlgorithmToCurve() { +// // Given/When +// let p256Curve = Flow.SignatureAlgorithm.ECDSA_P256.WCCurve +// let secp256k1Curve = Flow.SignatureAlgorithm.ECDSA_SECP256k1.WCCurve +// let unknownCurve = Flow.SignatureAlgorithm.unknown.WCCurve +// +// // Then +// #expect(p256Curve == Curve.nist256p1) +// #expect(secp256k1Curve == Curve.secp256k1) +// #expect(unknownCurve == nil) +// } + + @Test + func testStringDropPrefix() { + // Given + let stringWithPrefix = "0x1234567890abcdef" + let stringWithoutPrefix = "1234567890abcdef" + + // When + let result1 = stringWithPrefix.dropPrefix("0x") + let result2 = stringWithoutPrefix.dropPrefix("0x") + + // Then + #expect(result1 == "1234567890abcdef") + #expect(result2 == "1234567890abcdef") + } + + @Test + func testChainIDKeyIndexerURL() { + // Given + let publicKey = "1234567890abcdef" + + // When + let mainnetURL = Flow.ChainID.mainnet.keyIndexer(with: publicKey) + let testnetURL = Flow.ChainID.testnet.keyIndexer(with: publicKey) + let emulatorURL = Flow.ChainID.emulator.keyIndexer(with: publicKey) + + // Then + #expect(mainnetURL?.absoluteString == "https://production.key-indexer.flow.com/key/1234567890abcdef") + #expect(testnetURL?.absoluteString == "https://staging.key-indexer.flow.com/key/1234567890abcdef") + #expect(emulatorURL == nil) // Emulator doesn't have a key indexer URL + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockAccessAPI.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockAccessAPI.swift new file mode 100644 index 0000000..d775f8e --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockAccessAPI.swift @@ -0,0 +1,180 @@ +// +// MockAccessAPI.swift +// FlowWalletKit +// +// Created by Marty Ulrich on 3/24/25. +// + +@testable import FlowWalletKit +import Flow +import Foundation +import BigInt + +public final class MockAccessAPI: FlowAccessProtocol { + private let accountsByAddress: [Flow.Address: Flow.Account] + + public init(accountsByAddress: [Flow.Address: Flow.Account]) { + self.accountsByAddress = accountsByAddress + } + + // MARK: - FlowAccessProtocol + + public func ping() async throws -> Bool { + true + } + + public func getLatestBlockHeader() async throws -> Flow.BlockHeader { + // Provide stubbed data + return Flow.BlockHeader( + id: Flow.ID(data: Data("latest-block-id".utf8)), + parentId: Flow.ID(data: Data("parent-block-id".utf8)), + height: 999, + timestamp: Date() + ) + } + + public func getBlockHeaderById(id: Flow.ID) async throws -> Flow.BlockHeader { + // Provide stubbed data + return Flow.BlockHeader( + id: id, + parentId: Flow.ID(data: Data("parent-block-id".utf8)), + height: 1000, + timestamp: Date() + ) + } + + public func getBlockHeaderByHeight(height: UInt64) async throws -> Flow.BlockHeader { + // Provide stubbed data + return Flow.BlockHeader( + id: Flow.ID(data: Data("block-header-id-\(height)".utf8)), + parentId: Flow.ID(data: Data("parent-block-id".utf8)), + height: height, + timestamp: Date() + ) + } + + public func getLatestBlock(sealed: Bool) async throws -> Flow.Block { + // Provide stubbed data + return Flow.Block( + id: Flow.ID(data: Data("latest-block-id".utf8)), + parentId: Flow.ID(data: Data("latest-parent-id".utf8)), + height: 999, + timestamp: Date(), + collectionGuarantees: [], + blockSeals: [], + signatures: [] + ) + } + + public func getBlockById(id: Flow.ID) async throws -> Flow.Block { + return Flow.Block( + id: id, + parentId: Flow.ID(data: Data("block-parent".utf8)), + height: 111, + timestamp: Date(), + collectionGuarantees: [], + blockSeals: [], + signatures: [] + ) + } + + public func getBlockByHeight(height: UInt64) async throws -> Flow.Block { + return Flow.Block( + id: Flow.ID(data: Data("block-id-\(height)".utf8)), + parentId: Flow.ID(data: Data("block-parent-\(height)".utf8)), + height: height, + timestamp: Date(), + collectionGuarantees: [], + blockSeals: [], + signatures: [] + ) + } + + public func getCollectionById(id: Flow.ID) async throws -> Flow.Collection { + return Flow.Collection( + id: id, + transactionIds: [] + ) + } + + public func sendTransaction(transaction: Flow.Transaction) async throws -> Flow.ID { + // Return some random or stubbed ID + return Flow.ID(data: Data("transaction-id".utf8)) + } + + public func getTransactionById(id: Flow.ID) async throws -> Flow.Transaction { + // Provide a stub transaction + return Flow.Transaction( + script: Flow.Script(text: "pub fun main() {}"), + arguments: [], + referenceBlockId: id, + gasLimit: BigUInt(999), + proposalKey: Flow.TransactionProposalKey(address: Flow.Address(hex: "0x01"), keyIndex: 0), + payer: Flow.Address(hex: "0x01"), + authorizers: [] + ) + } + + public func getTransactionResultById(id: Flow.ID) async throws -> Flow.TransactionResult { + // Provide a stub result + return Flow.TransactionResult( + status: .sealed, + errorMessage: "", + events: [], + statusCode: 0, + blockId: id, + computationUsed: "100" + ) + } + + public func getAccountAtLatestBlock(address: Flow.Address) async throws -> Flow.Account { + guard let account = accountsByAddress[address] else { + throw WalletError.invaildWalletType + } + return account + } + + public func getAccountByBlockHeight(address: Flow.Address, height: UInt64) async throws -> Flow.Account { + // Could return the same as getAccountAtLatestBlock, or some variant + guard let account = accountsByAddress[address] else { + throw WalletError.invaildWalletType + } + return account + } + + public func executeScriptAtLatestBlock(script: Flow.Script, arguments: [Flow.Argument]) async throws -> Flow.ScriptResponse { + Flow.ScriptResponse(data: Data("script-response".utf8)) + } + + public func executeScriptAtLatestBlock(script: Flow.Script, arguments: [Flow.Cadence.FValue]) async throws -> Flow.ScriptResponse { + Flow.ScriptResponse(data: Data("script-response".utf8)) + } + + public func executeScriptAtBlockId(script: Flow.Script, blockId: Flow.ID, arguments: [Flow.Argument]) async throws -> Flow.ScriptResponse { + Flow.ScriptResponse(data: Data("script-response".utf8)) + } + + public func executeScriptAtBlockId(script: Flow.Script, blockId: Flow.ID, arguments: [Flow.Cadence.FValue]) async throws -> Flow.ScriptResponse { + Flow.ScriptResponse(data: Data("script-response".utf8)) + } + + public func executeScriptAtBlockHeight(script: Flow.Script, height: UInt64, arguments: [Flow.Argument]) async throws -> Flow.ScriptResponse { + Flow.ScriptResponse(data: Data("script-response".utf8)) + } + + public func executeScriptAtBlockHeight(script: Flow.Script, height: UInt64, arguments: [Flow.Cadence.FValue]) async throws -> Flow.ScriptResponse { + Flow.ScriptResponse(data: Data("script-response".utf8)) + } + + public func getEventsForHeightRange(type: String, range: ClosedRange) async throws -> [Flow.Event.Result] { + [] + } + + public func getEventsForBlockIds(type: String, ids: Set) async throws -> [Flow.Event.Result] { + [] + } + + public func getNetworkParameters() async throws -> Flow.ChainID { + .mainnet + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockNetwork.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockNetwork.swift new file mode 100644 index 0000000..aff2c29 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockNetwork.swift @@ -0,0 +1,94 @@ +import Foundation +import Flow +@testable import FlowWalletKit + +struct MockNetwork { + static var shouldThrow = false + static var errorToThrow: Error = WalletError.keyIndexerRequestFailed + static var mockKeyIndexerResponse: KeyIndexerResponse? + static var mockFlowAccounts: [Flow.Account] = [] + static var mockKeyIndexerAccounts: [KeyIndexerResponse.Account] = [] + + static func resetMocks() { + shouldThrow = false + errorToThrow = WalletError.keyIndexerRequestFailed + mockKeyIndexerResponse = nil + mockFlowAccounts = [] + mockKeyIndexerAccounts = [] + } + + static func findAccount(publicKey: String, chainID: Flow.ChainID) async throws -> KeyIndexerResponse { + if shouldThrow { + throw errorToThrow + } + + if let response = mockKeyIndexerResponse { + return response + } + + // Create a default mock response + return KeyIndexerResponse( + publicKey: publicKey, + accounts: mockKeyIndexerAccounts.isEmpty ? + [KeyIndexerResponse.Account( + address: "0x1234567890abcdef", + keyId: 0, + weight: 1000, + sigAlgo: 1, + hashAlgo: 3, + signing: .ECDSA_P256, + hashing: .SHA3_256, + isRevoked: false + )] : mockKeyIndexerAccounts + ) + } + + static func findAccountByKey(publicKey: String, chainID: Flow.ChainID) async throws -> [KeyIndexerResponse.Account] { + if shouldThrow { + throw errorToThrow + } + + if !mockKeyIndexerAccounts.isEmpty { + return mockKeyIndexerAccounts + } + + return [ + KeyIndexerResponse.Account( + address: "0x1234567890abcdef", + keyId: 0, + weight: 1000, + sigAlgo: 1, + hashAlgo: 3, + signing: .ECDSA_P256, + hashing: .SHA3_256, + isRevoked: false + ) + ] + } + + static func findFlowAccountByKey(publicKey: String, chainID: Flow.ChainID) async throws -> [Flow.Account] { + if shouldThrow { + throw errorToThrow + } + + if !mockFlowAccounts.isEmpty { + return mockFlowAccounts + } + + return [ + Flow.Account( + address: Flow.Address(hex: "0x1234567890abcdef"), + keys: [ + Flow.AccountKey( + index: 0, + publicKey: Flow.PublicKey(hex: publicKey), + signAlgo: .ECDSA_P256, + hashAlgo: .SHA3_256, + weight: 1000, + revoked: false + ) + ] + ) + ] + } +} \ No newline at end of file diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockPrivateKey.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockPrivateKey.swift new file mode 100644 index 0000000..bad2f4d --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockPrivateKey.swift @@ -0,0 +1,72 @@ +import Foundation +import Flow +@testable import FlowWalletKit +import Factory + +class MockPrivateKey: KeyProtocol { + @Injected(\.keychainStorage) var storage + var keyType: KeyType = .privateKey + var mockPublicKey: Data + var mockPrivateKey: Data + var signatureToReturn: Data + var shouldThrowOnSign: Bool = false + var errorToThrow: Error = WalletError.signError + + init(mockPublicKey: Data = Data(repeating: 1, count: 65), + mockPrivateKey: Data = Data(repeating: 3, count: 32), + signatureToReturn: Data = Data(repeating: 2, count: 64)) { + self.mockPublicKey = mockPublicKey + self.mockPrivateKey = mockPrivateKey + self.signatureToReturn = signatureToReturn + } + + static func create(_ advance: String) throws -> MockPrivateKey { + MockPrivateKey() + } + + static func create() throws -> MockPrivateKey { + MockPrivateKey() + } + + static func createAndStore(id: String, password: String) throws -> MockPrivateKey { + MockPrivateKey() + } + + static func get(id: String, password: String) throws -> MockPrivateKey { + MockPrivateKey() + } + + static func restore(secret: Data) throws -> MockPrivateKey { + MockPrivateKey() + } + + func store(id: String, password: String) throws { + // Mock implementation + } + + func publicKey(signAlgo: Flow.SignatureAlgorithm) -> Data? { + return mockPublicKey + } + + func privateKey(signAlgo: Flow.SignatureAlgorithm) -> Data? { + return mockPrivateKey + } + + func isValidSignature(signature: Data, message: Data, signAlgo: Flow.SignatureAlgorithm) -> Bool { + return true // Always return true for testing + } + + func sign(data: Data, signAlgo: Flow.SignatureAlgorithm, hashAlgo: Flow.HashAlgorithm) throws -> Data { + if shouldThrowOnSign { + throw errorToThrow + } + return signatureToReturn + } + + func rawSign(data: Data, signAlgo: Flow.SignatureAlgorithm) throws -> Data { + if shouldThrowOnSign { + throw errorToThrow + } + return signatureToReturn + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockSecureEnclaveKey.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockSecureEnclaveKey.swift new file mode 100644 index 0000000..6f911ee --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockSecureEnclaveKey.swift @@ -0,0 +1,73 @@ +import Foundation +import CryptoKit +import Flow +@testable import FlowWalletKit +import Factory + +class MockSecureEnclaveKey: KeyProtocol { + var keyType: KeyType = .secureEnclave + @Injected(\.keychainStorage) var storage + var mockPublicKey: Data + var signatureToReturn: Data + var shouldThrowOnSign: Bool = false + var errorToThrow: Error = WalletError.signError + + init(mockPublicKey: Data = Data(repeating: 1, count: 32), signatureToReturn: Data = Data(repeating: 2, count: 64)) { + self.mockPublicKey = mockPublicKey + self.signatureToReturn = signatureToReturn + } + + static func create(_ advance: String) throws -> MockSecureEnclaveKey { + MockSecureEnclaveKey() + } + + static func create() throws -> MockSecureEnclaveKey { + MockSecureEnclaveKey() + } + + static func createAndStore(id: String, password: String) throws -> MockSecureEnclaveKey { + return MockSecureEnclaveKey(mockPublicKey: Data(repeating: 1, count: 32), signatureToReturn: Data(repeating: 2, count: 64)) + } + + static func get(id: String, password: String) throws -> MockSecureEnclaveKey { + return MockSecureEnclaveKey(mockPublicKey: Data(repeating: 1, count: 32), signatureToReturn: Data(repeating: 2, count: 64)) + } + + static func restore(secret: Data) throws -> MockSecureEnclaveKey { + return MockSecureEnclaveKey(mockPublicKey: Data(repeating: 1, count: 32), signatureToReturn: Data(repeating: 2, count: 64)) + } + + func store(id: String, password: String) throws { + // Mock implementation + } + + func publicKey(signAlgo: Flow.SignatureAlgorithm) -> Data? { + return mockPublicKey + } + + func privateKey(signAlgo: Flow.SignatureAlgorithm) -> Data? { + return nil // Simulating SecureEnclave behavior + } + + func isValidSignature(signature: Data, message: Data, signAlgo: Flow.SignatureAlgorithm) -> Bool { + return true // Always return true for testing + } + + func sign(data: Data, signAlgo: Flow.SignatureAlgorithm, hashAlgo: Flow.HashAlgorithm) throws -> Data { + if shouldThrowOnSign { + throw errorToThrow + } + return signatureToReturn + } + + func rawSign(data: Data, signAlgo: Flow.SignatureAlgorithm) throws -> Data { + if shouldThrowOnSign { + throw errorToThrow + } + return signatureToReturn + } + + func create(storage: any FlowWalletKit.StorageProtocol) throws -> any FlowWalletKit.KeyProtocol { + return self + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockSeedPhraseKey.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockSeedPhraseKey.swift new file mode 100644 index 0000000..531bb78 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockSeedPhraseKey.swift @@ -0,0 +1,74 @@ +import Foundation +import Flow +@testable import FlowWalletKit +import Factory + +class MockSeedPhraseKey: KeyProtocol { + var keyType: KeyType = .seedPhrase + @Injected(\.keychainStorage) var storage + var mockPublicKey: Data + var mockPrivateKey: Data + var signatureToReturn: Data + var shouldThrowOnSign: Bool = false + var errorToThrow: Error = WalletError.signError + var mnemonic: String = "test test test test test test test test test test test test" + var derivationPath: String = "m/44'/539'/0'/0/0" + + init(mockPublicKey: Data = Data(repeating: 1, count: 65), + mockPrivateKey: Data = Data(repeating: 3, count: 32), + signatureToReturn: Data = Data(repeating: 2, count: 64)) { + self.mockPublicKey = mockPublicKey + self.mockPrivateKey = mockPrivateKey + self.signatureToReturn = signatureToReturn + } + + static func create(_ advance: String) throws -> MockSeedPhraseKey { + MockSeedPhraseKey() + } + + static func create() throws -> MockSeedPhraseKey { + MockSeedPhraseKey() + } + + static func createAndStore(id: String, password: String) throws -> MockSeedPhraseKey { + MockSeedPhraseKey() + } + + static func get(id: String, password: String) throws -> MockSeedPhraseKey { + MockSeedPhraseKey() + } + + static func restore(secret: Data) throws -> MockSeedPhraseKey { + MockSeedPhraseKey() + } + + func store(id: String, password: String) throws { + // Mock implementation + } + + func publicKey(signAlgo: Flow.SignatureAlgorithm) -> Data? { + return mockPublicKey + } + + func privateKey(signAlgo: Flow.SignatureAlgorithm) -> Data? { + return mockPrivateKey + } + + func isValidSignature(signature: Data, message: Data, signAlgo: Flow.SignatureAlgorithm) -> Bool { + return true // Always return true for testing + } + + func sign(data: Data, signAlgo: Flow.SignatureAlgorithm, hashAlgo: Flow.HashAlgorithm) throws -> Data { + if shouldThrowOnSign { + throw errorToThrow + } + return signatureToReturn + } + + func rawSign(data: Data, signAlgo: Flow.SignatureAlgorithm) throws -> Data { + if shouldThrowOnSign { + throw errorToThrow + } + return signatureToReturn + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockStorage.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockStorage.swift new file mode 100644 index 0000000..874b1f3 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockStorage.swift @@ -0,0 +1,46 @@ +import Foundation +import FlowWalletKit + +class MockStorage: StorageProtocol { + var storage: [String: Data] = [:] + var errorToThrow: Error? + + var allKeys: [String] { + return Array(storage.keys) + } + + func findKey(_ keyword: String) throws -> [String] { + if let error = errorToThrow { + throw error + } + return storage.keys.filter { $0.contains(keyword) } + } + + func get(_ key: String) throws -> Data? { + if let error = errorToThrow { + throw error + } + return storage[key] + } + + func set(_ key: String, value: Data) throws { + if let error = errorToThrow { + throw error + } + storage[key] = value + } + + func remove(_ key: String) throws { + if let error = errorToThrow { + throw error + } + storage.removeValue(forKey: key) + } + + func removeAll() throws { + if let error = errorToThrow { + throw error + } + storage.removeAll() + } +} \ No newline at end of file diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockWallet.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockWallet.swift new file mode 100644 index 0000000..6e6e580 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Helpers/Mocks/MockWallet.swift @@ -0,0 +1,61 @@ +import Foundation +import Flow +@testable import FlowWalletKit + +class MockWallet { + var type: WalletType + var networks: Set + var mockAccounts: [Flow.ChainID: [Account]] + var mockFlowAccounts: [Flow.ChainID: [Flow.Account]] + var shouldThrowOnFetch: Bool = false + var errorToThrow: Error = WalletError.loadCacheFailed + + init(type: WalletType, + networks: Set = [.mainnet, .testnet], + mockAccounts: [Flow.ChainID: [Account]] = [:], + mockFlowAccounts: [Flow.ChainID: [Flow.Account]] = [:]) { + self.type = type + self.networks = networks + self.mockAccounts = mockAccounts + self.mockFlowAccounts = mockFlowAccounts + } + + func fetchAccount() async { + // Mock implementation + } + + func fetchAllNetworkAccounts() async throws -> [Flow.ChainID: [Account]] { + if shouldThrowOnFetch { + throw errorToThrow + } + return mockAccounts + } + + func account(chainID: Flow.ChainID) async throws -> [Flow.Account] { + if shouldThrowOnFetch { + throw errorToThrow + } + return mockFlowAccounts[chainID] ?? [] + } + + func fullAccount(chainID: Flow.ChainID) async throws -> [Flow.Account] { + if shouldThrowOnFetch { + throw errorToThrow + } + return mockFlowAccounts[chainID] ?? [] + } + + func cache() throws { + if shouldThrowOnFetch { + throw errorToThrow + } + // Mock implementation + } + + func loadCahe() throws { + if shouldThrowOnFetch { + throw errorToThrow + } + // Mock implementation + } +} \ No newline at end of file diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Keys/PrivateKeyTests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Keys/PrivateKeyTests.swift new file mode 100644 index 0000000..55a78bf --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Keys/PrivateKeyTests.swift @@ -0,0 +1,78 @@ +import Foundation +import Testing +import Flow +@testable import FlowWalletKit +import Factory + +struct PrivateKeyTests { + + init() { + Container.shared.keychainStorage.register { MockStorage() } + } + + @Test + func testPublicKey() throws { + // Given + let privateKey = PrivateKey() + + // When + let publicKey = privateKey.publicKey(signAlgo: .ECDSA_P256) + + // Then + #expect(publicKey != nil) + if let key = publicKey { + #expect(key.count == 64) + } + } + + @Test + func testPrivateKey() throws { + // Given + let privateKey = PrivateKey() + + // When + let privateKeyPrivateKey = privateKey.privateKey(signAlgo: .ECDSA_P256) + + // Then + #expect(privateKeyPrivateKey != nil) + if let key = privateKeyPrivateKey { + #expect(key.count == 32) + } + } + + @Test + func testSigningData() throws { + // Given + let privateKey = PrivateKey() + let dataToSign = "test data".data(using: .utf8)! + + // When + let signature = try privateKey.sign(data: dataToSign, signAlgo: .ECDSA_P256, hashAlgo: .SHA2_256) + + // Then + #expect(signature.count == 64) + } + + @Test + func testValidateSignature() { + // Given + let privateKey = PrivateKey() + let message = "test message".data(using: .utf8)! + let signature = Data(repeating: 3, count: 64) + + // When + let isValid = privateKey.isValidSignature(signature: signature, message: message, signAlgo: .ECDSA_P256) + + // Then + #expect(isValid == false) + } + + @Test + func testStoreOperation() throws { + // Given + let privateKey = PrivateKey() + + // When/Then - should not throw + try privateKey.store(id: "test_id", password: "password") + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Keys/SecureEnclaveKeyTests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Keys/SecureEnclaveKeyTests.swift new file mode 100644 index 0000000..bbf6377 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Keys/SecureEnclaveKeyTests.swift @@ -0,0 +1,63 @@ +import Foundation +import Testing +import Flow +@testable import FlowWalletKit +import CryptoKit + +struct SecureEnclaveKeyTests { + let secureEnclaveKey: SecureEnclaveKey + + init() throws { + secureEnclaveKey = SecureEnclaveKey(key: try SecureEnclave.P256.Signing.PrivateKey()) + } + + @Test + func testPublicKey() throws { + // When + let publicKey = secureEnclaveKey.publicKey(signAlgo: .ECDSA_P256) + + // Then + #expect(publicKey != nil) + if let key = publicKey { + #expect(key.count == 64) + } + } + + @Test + func testSigningData() throws { + let dataToSign = "test data".data(using: .utf8)! + + // When + let signature = try secureEnclaveKey.sign(data: dataToSign, signAlgo: .ECDSA_P256, hashAlgo: .SHA2_256) + + // Then + #expect(signature.count == 64) + } + + @Test + func testValidateSignature() { + let message = "test message".data(using: .utf8)! + let signature = Data(repeating: 3, count: 64) + + // When + let isValid = secureEnclaveKey.isValidSignature(signature: signature, message: message, signAlgo: .ECDSA_P256) + + // Then + #expect(isValid == false) + } + + @Test + func testStoreOperation() throws { + // When/Then - should not throw + try secureEnclaveKey.store(id: "test_id", password: "password") + } + + @Test + func testNoPrivateKeyAccess() { + // When + let privateKey = secureEnclaveKey.privateKey(signAlgo: .ECDSA_P256) + + // Then - should be nil since Secure Enclave doesn't expose private keys + #expect(privateKey == nil) + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Keys/SeedPhraseKeyTests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Keys/SeedPhraseKeyTests.swift new file mode 100644 index 0000000..b1776f3 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Keys/SeedPhraseKeyTests.swift @@ -0,0 +1,81 @@ +import Foundation +import Testing +import Flow +@testable import FlowWalletKit +import WalletCore +import Factory + +struct SeedPhraseKeyTests { + var mockHDWallet: HDWallet! + + init() { + mockHDWallet = HDWallet(strength: defaultSeedPhraseLength.strength, passphrase: "")! + Container.shared.keychainStorage.register { MockStorage() } + } + + @Test + func testPublicKey() throws { + // Given + let seedPhraseKey = SeedPhraseKey(hdWallet: mockHDWallet) + + // When + let publicKey = seedPhraseKey.publicKey(signAlgo: .ECDSA_P256) + + // Then + #expect(publicKey != nil) + if let key = publicKey { + #expect(key.count == 64) + } + } + + @Test + func testPrivateKey() throws { + // Given + let seedPhraseKey = SeedPhraseKey(hdWallet: mockHDWallet) + + // When + let privateKey = seedPhraseKey.privateKey(signAlgo: .ECDSA_P256) + + // Then + #expect(privateKey != nil) + if let key = privateKey { + #expect(key.count == 32) + } + } + + @Test + func testSigningData() throws { + // Given + let seedPhraseKey = SeedPhraseKey(hdWallet: mockHDWallet) + let dataToSign = "test data".data(using: .utf8)! + + // When + let signature = try seedPhraseKey.sign(data: dataToSign, signAlgo: .ECDSA_P256, hashAlgo: .SHA2_256) + + // Then + #expect(signature.count == 64) + } + + @Test + func testValidateSignature() { + // Given + let seedPhraseKey = SeedPhraseKey(hdWallet: mockHDWallet) + let message = "test message".data(using: .utf8)! + let signature = Data(repeating: 3, count: 64) + + // When + let isValid = seedPhraseKey.isValidSignature(signature: signature, message: message, signAlgo: .ECDSA_P256) + + // Then + #expect(isValid == false) + } + + @Test + func testStoreOperation() throws { + // Given + let seedPhraseKey = SeedPhraseKey(hdWallet: mockHDWallet) + + // When/Then - should not throw + try seedPhraseKey.store(id: "test_id", password: "password") + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/NetworkTests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/NetworkTests.swift new file mode 100644 index 0000000..e1d0ffe --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/NetworkTests.swift @@ -0,0 +1,65 @@ +import Foundation +import Testing +import Flow +@testable import FlowWalletKit + +struct NetworkTests { + + func testFindAccount() async throws { + // This test would require network access, so we'll focus on verifying that + // the correct URL is constructed for the key indexer + + // Given + let testPublicKey = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + let chainID = Flow.ChainID.testnet + + // When + guard let url = chainID.keyIndexer(with: testPublicKey) else { + #expect(false, "Failed to construct key indexer URL") + return + } + + // Then + #expect(url.absoluteString.contains(testPublicKey)) + #expect(url.absoluteString.contains("testnet")) + } + + func testKeyIndexerResponseAccountMapping() { + // Given + let publicKey = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + let keyIndexerAccounts = [ + KeyIndexerResponse.Account( + address: "0x1234", + keyId: 0, + weight: 1000, + sigAlgo: 1, + hashAlgo: 3, + signing: .ECDSA_P256, + hashing: .SHA3_256, + isRevoked: false + ), + KeyIndexerResponse.Account( + address: "0x5678", + keyId: 0, + weight: 1000, + sigAlgo: 1, + hashAlgo: 3, + signing: .ECDSA_P256, + hashing: .SHA3_256, + isRevoked: false + ) + ] + + let response = KeyIndexerResponse(publicKey: publicKey, accounts: keyIndexerAccounts) + + // When + let flowAccounts = response.accountResponse + + // Then + #expect(flowAccounts.count == 2) + #expect(flowAccounts[0].address.hex == "0x1234") + #expect(flowAccounts[1].address.hex == "0x5678") + #expect(flowAccounts[0].keys.count == 1) + #expect(flowAccounts[0].keys[0].publicKey.hex == publicKey) + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/SEWalletTests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/SEWalletTests.swift index 1ac8fc4..7633500 100644 --- a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/SEWalletTests.swift +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/SEWalletTests.swift @@ -1,20 +1,20 @@ -@testable import FlowWalletKit -import XCTest - -final class FlowWalletCoreTests: XCTestCase { - let id = "userId" - let password = "password" - -// func testSecureEnclaveKeyCreate() throws { -// let wallet = try SecureEnclaveKey.create(id: id, password: password, sync: false) -// let reWallet = try SecureEnclaveKey.get(id: id, password: password) -// XCTAssertEqual(try wallet.publicKey(), try reWallet.publicKey()) -// } +//@testable import FlowWalletKit +//import XCTest // -// func testSecureEnclaveKeyStore() throws { -// let wallet = try SecureEnclaveKey.create() -// try wallet.store(id: id, password: password, sync: false) -// let reWallet = try SecureEnclaveKey.get(id: id, password: password) -// XCTAssertEqual(try wallet.publicKey(), try reWallet.publicKey()) -// } -} +//final class FlowWalletCoreTests: XCTestCase { +// let id = "userId" +// let password = "password" +// +//// func testSecureEnclaveKeyCreate() throws { +//// let wallet = try SecureEnclaveKey.create(id: id, password: password, sync: false) +//// let reWallet = try SecureEnclaveKey.get(id: id, password: password) +//// XCTAssertEqual(try wallet.publicKey(), try reWallet.publicKey()) +//// } +//// +//// func testSecureEnclaveKeyStore() throws { +//// let wallet = try SecureEnclaveKey.create() +//// try wallet.store(id: id, password: password, sync: false) +//// let reWallet = try SecureEnclaveKey.get(id: id, password: password) +//// XCTAssertEqual(try wallet.publicKey(), try reWallet.publicKey()) +//// } +//} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Storage/KeychainStorageTests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Storage/KeychainStorageTests.swift new file mode 100644 index 0000000..988559a --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Storage/KeychainStorageTests.swift @@ -0,0 +1,88 @@ +import Foundation +import Testing +@testable import FlowWalletKit +import KeychainAccess + +//TODO: these are failing because we don't have keychain access in SPM tests. (technically this would be an "integration test", but stubbing the Keychain makes KeychainStorage do very little) + +struct KeychainStorageTests { + + @Test + func testSetAndGetData() throws { + // Given + let storage = KeychainStorage(service: "com.test.temp", label: "TempTest", synchronizable: false) + try? storage.removeAll() // Clear any previous test data + + let key = "testKey" + let testData = "testValue".data(using: .utf8)! + + // When + try storage.set(key, value: testData) + let retrievedData = try storage.get(key) + + // Then + #expect(retrievedData != nil) + if let data = retrievedData { + #expect(data == testData) + } + + // Clean up + try storage.removeAll() + } + + @Test + func testRemoveKey() throws { + // Given + let storage = KeychainStorage(service: "com.test.temp", label: "TempTest", synchronizable: false) + try? storage.removeAll() // Clear any previous test data + + let key = "testKeyToRemove" + let testData = "testValue".data(using: .utf8)! + try storage.set(key, value: testData) + + // When + try storage.remove(key) + let retrievedData = try storage.get(key) + + // Then + #expect(retrievedData == nil) + } + + @Test + func testFindKey() throws { + // Given + let storage = KeychainStorage(service: "com.test.temp", label: "TempTest", synchronizable: false) + try? storage.removeAll() // Clear any previous test data + + try storage.set("prefix_key1", value: Data()) + try storage.set("prefix_key2", value: Data()) + try storage.set("other_key", value: Data()) + + // When + let keys = try storage.findKey("prefix") + + // Then + #expect(keys.count == 2) + #expect(keys.contains("prefix_key1")) + #expect(keys.contains("prefix_key2")) + #expect(!keys.contains("other_key")) + + // Clean up + try storage.removeAll() + } + + @Test + func testRemoveAll() throws { + // Given + let storage = KeychainStorage(service: "com.test.temp", label: "TempTest", synchronizable: false) + + try storage.set("key1", value: Data()) + try storage.set("key2", value: Data()) + + // When + try storage.removeAll() + + // Then + #expect(storage.allKeys.isEmpty) + } +} diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Wallet/WalletTests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Wallet/WalletTests.swift new file mode 100644 index 0000000..bd6f2e0 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/Wallet/WalletTests.swift @@ -0,0 +1,52 @@ +import Foundation +import Testing +import Flow +@testable import FlowWalletKit + +struct WalletTests { + + @Test + func testWalletInitialization() { + // Given + let mockKey = MockPrivateKey() + let walletType = WalletType.key(mockKey) + + // When + let wallet = Wallet(type: walletType, networks: [.testnet]) + + // Then + #expect(wallet.type.id.starts(with: "Key/")) + #expect(wallet.networks.count == 1) + #expect(wallet.networks.contains(.testnet)) + } + + @Test + func testAddNetwork() { + // Given + let mockKey = MockPrivateKey() + let walletType = WalletType.key(mockKey) + let wallet = Wallet(type: walletType, networks: [.testnet]) + + // When + wallet.addNetwork(.mainnet) + + // Then + #expect(wallet.networks.count == 2) + #expect(wallet.networks.contains(.testnet)) + #expect(wallet.networks.contains(.mainnet)) + } + + @Test + func testWatchWalletInitialization() { + // Given + let address = Flow.Address(hex: "0x1234567890abcdef") + let walletType = WalletType.watch(address) + + // When + let wallet = Wallet(type: walletType) + + // Then + #expect(wallet.type.id.starts(with: "Watch/")) + #expect(wallet.type.id.contains(address.hex)) + } +} diff --git a/iOS/FlowWalletKit/Tests/README.md b/iOS/FlowWalletKit/Tests/README.md new file mode 100644 index 0000000..1faee79 --- /dev/null +++ b/iOS/FlowWalletKit/Tests/README.md @@ -0,0 +1,63 @@ +# FlowWalletKit Tests + +This directory contains unit tests for the FlowWalletKit package. The tests are written using the Swift Testing framework. + +## Running Tests + +To run the tests, use the following steps: + +1. Open the project in Xcode +2. Make sure to select an iOS Simulator as the target device (tests will not run on physical devices) +3. Select "Product > Test" or use the keyboard shortcut ⌘U + +## Test Structure + +The tests are organized according to the components they test: + +- **Keys**: Tests for the various key implementations (SecureEnclaveKey, PrivateKey, SeedPhraseKey) +- **Storage**: Tests for the storage implementations (KeychainStorage) +- **Wallet**: Tests for the Wallet implementation +- **Crypto**: Tests for cryptographic utilities (BIP39, SymmetricEncryption) + +## Mocks and Stubs + +Mock implementations are provided in the `Mocks` directory. These are used to isolate components for testing and avoid external dependencies. + +## Best Practices + +When writing tests: + +1. Use descriptive test names that explain what is being tested +2. Follow the "Given-When-Then" pattern for test structure +3. Mock external dependencies +4. Ensure tests run in isolation and don't depend on external services +5. Only test on iOS Simulator to ensure consistency + +## Adding New Tests + +When adding new tests: + +1. Create a new test file in the appropriate directory +2. Import the Testing framework +3. Create a struct containing test methods marked with @Test +4. Follow the existing patterns for test implementation + +Example: + +```swift +import Foundation +@testable import FlowWalletKit + +struct MyComponentTests { + + func testFeature() { + // Given + let component = MyComponent() + + // When + let result = component.doSomething() + + // Then + #expect(result == expectedResult) + } +}