diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 90419f00..25a5f1dc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,7 @@ jobs: languages: "swift" - name: Test - run: xcodebuild -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15" + run: xcodebuild -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15" -resultBundlePath "~/xcode-$NOW.xcresult" - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/docs-and-deploy.yml b/.github/workflows/docs-and-deploy.yml index 049bc81d..6be4218b 100644 --- a/.github/workflows/docs-and-deploy.yml +++ b/.github/workflows/docs-and-deploy.yml @@ -5,7 +5,7 @@ on: - main jobs: build: - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v4.1.1 diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 8ef4691d..e14472bd 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -12,11 +12,11 @@ on: jobs: deploy: name: Running unit tests - runs-on: macos-14 + runs-on: macos-15 steps: - name: Select Xcode version #run: sudo xcode-select -s '/Applications/Xcode_14.3.1.app/Contents/Developer' - run: sudo xcode-select -s '/Applications/Xcode_15.2.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_16.app/Contents/Developer' - name: Checkout repository uses: actions/checkout@v4.1.1 @@ -62,7 +62,7 @@ jobs: - name: Test #run: xcodebuild test -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=16.4,name=iPhone 14" -enableCodeCoverage YES -resultBundlePath "~/xcode-$NOW.xcresult" - run: xcodebuild test -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15" -enableCodeCoverage YES -resultBundlePath "~/xcode-$NOW.xcresult" + run: xcodebuild test -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=18.0,name=iPhone 16" -enableCodeCoverage YES -resultBundlePath "~/xcode-$NOW.xcresult" diff --git a/Package.swift b/Package.swift index 05472fb2..354041d1 100644 --- a/Package.swift +++ b/Package.swift @@ -13,9 +13,9 @@ let package = Package( ], dependencies: [ .package(name: "KukaiCryptoSwift", url: "https://github.com/kukai-wallet/kukai-crypto-swift", from: "1.0.23" /*.branch("develop")*/), - .package(name: "CustomAuth", url: "https://github.com/torusresearch/customauth-swift-sdk", from: "10.0.1"), + .package(name: "CustomAuth", url: "https://github.com/torusresearch/customauth-swift-sdk", from: "10.0.2"), .package(name: "SignalRClient", url: "https://github.com/moozzyk/SignalR-Client-Swift", from: "0.8.0"), - .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.18.10") + .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.19.3") ], targets: [ .target( diff --git a/Sources/KukaiCoreSwift/Clients/TezosDomainsClient.swift b/Sources/KukaiCoreSwift/Clients/TezosDomainsClient.swift index 92298235..e11aa5f2 100644 --- a/Sources/KukaiCoreSwift/Clients/TezosDomainsClient.swift +++ b/Sources/KukaiCoreSwift/Clients/TezosDomainsClient.swift @@ -86,7 +86,7 @@ public class TezosDomainsClient { dispatchGroup.leave() } - getDomainFor(address: address, url: TezosNodeClientConfig.defaultTestnetURLs.tezosDomainsURL) { result in + getDomainFor(address: address, url: TezosNodeClientConfig.defaultGhostnetURLs.tezosDomainsURL) { result in guard let res = try? result.get() else { errorGhost = result.getFailure() dispatchGroup.leave() @@ -172,7 +172,7 @@ public class TezosDomainsClient { dispatchGroup.leave() } - getDomainsFor(addresses: addresses, url: TezosNodeClientConfig.defaultTestnetURLs.tezosDomainsURL) { result in + getDomainsFor(addresses: addresses, url: TezosNodeClientConfig.defaultGhostnetURLs.tezosDomainsURL) { result in guard let res = try? result.get() else { errorGhost = result.getFailure() dispatchGroup.leave() diff --git a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift index 7aee51c5..48920fe4 100644 --- a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift +++ b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift @@ -18,6 +18,7 @@ public class TzKTClient { /// Unique Errors that TzKTClient can throw public enum TzKTServiceError: Error { case invalidURL + case invalidAddress case parseError(String) } @@ -322,7 +323,8 @@ public class TzKTClient { var estimatedNextReward: RewardDetails? = nil // Check if we have enough rewards to bring us up to the current cycle - guard currentDelegatorRewards.count > TzKTClient.numberOfFutureCyclesReturned, + guard TzKTClient.numberOfFutureCyclesReturned >= 0, + currentDelegatorRewards.count > TzKTClient.numberOfFutureCyclesReturned, let currentBakerConfig = bakerConfigs[delegate.address], let inProgresCycleBakerConfig = bakerConfigs[currentDelegatorRewards[TzKTClient.numberOfFutureCyclesReturned].baker.address] else { @@ -713,6 +715,11 @@ public class TzKTClient { - parameter completion: The completion block called with a `Result` containing the number or an error */ public func getBalanceCount(forAddress: String, completion: @escaping (Result) -> Void) { + guard forAddress != "" else { + completion(Result.failure(KukaiError.internalApplicationError(error: TzKTServiceError.invalidAddress))) + return + } + var url = config.tzktURL url.appendPathComponent("v1/tokens/balances/count") url.appendQueryItem(name: "account", value: forAddress) @@ -730,6 +737,11 @@ public class TzKTClient { - parameter completion: The completion block called with a `Result` containing an array of balances or an error */ public func getBalancePage(forAddress: String, offset: Int = 0, completion: @escaping ((Result<[TzKTBalance], KukaiError>) -> Void)) { + guard forAddress != "" else { + completion(Result.failure(KukaiError.internalApplicationError(error: TzKTServiceError.invalidAddress))) + return + } + var url = config.tzktURL url.appendPathComponent("v1/tokens/balances") url.appendQueryItem(name: "account", value: forAddress) @@ -749,6 +761,11 @@ public class TzKTClient { - parameter completion: The completion block called with a `Result` containing an object or an error */ public func getAccount(forAddress: String, fromURL: URL? = nil, completion: @escaping ((Result) -> Void)) { + guard forAddress != "" else { + completion(Result.failure(KukaiError.internalApplicationError(error: TzKTServiceError.invalidAddress))) + return + } + var url = fromURL == nil ? config.tzktURL : fromURL url?.appendPathComponent("v1/accounts/\(forAddress)") @@ -768,6 +785,11 @@ public class TzKTClient { - parameter completion: The completion block called with a `Result` containing an object or an error */ public func getAllBalances(forAddress address: String, completion: @escaping ((Result) -> Void)) { + guard address != "" else { + completion(Result.failure(KukaiError.internalApplicationError(error: TzKTServiceError.invalidAddress))) + return + } + getBalanceCount(forAddress: address) { [weak self] result in guard let tokenCount = try? result.get() else { completion(Result.failure(result.getFailure())) @@ -901,7 +923,7 @@ public class TzKTClient { continue } else if balance.token.metadata != nil { // Else create a Token object and put into array, if we have valid metadata (e.g. able to tell how many decimals it has) - tokens.append(Token(from: balance.token, andTokenAmount: balance.tokenAmount)) + tokens.append(Token(from: balance, andTokenAmount: balance.tokenAmount)) } } @@ -958,6 +980,11 @@ public class TzKTClient { /// Fetch all transactions, both account operations, and token transfers, and combine them into 1 response public func fetchTransactions(forAddress address: String, limit: Int = 50, completion: @escaping (([TzKTTransaction]) -> Void)) { + guard address != "" else { + completion([]) + return + } + let dispatchGroupTransactions = DispatchGroup() dispatchGroupTransactions.enter() dispatchGroupTransactions.enter() @@ -1121,6 +1148,7 @@ public class TzKTClient { extension TzKTClient: HubConnectionDelegate { public func connectionDidOpen(hubConnection: HubConnection) { + if addressesToWatch.count == 0 { return } // Request to be subscribed to events belonging to the given account let subscription = AccountSubscription(addresses: addressesToWatch) diff --git a/Sources/KukaiCoreSwift/Models/BakingBad/TzKTBalance.swift b/Sources/KukaiCoreSwift/Models/BakingBad/TzKTBalance.swift index be662982..aba09844 100644 --- a/Sources/KukaiCoreSwift/Models/BakingBad/TzKTBalance.swift +++ b/Sources/KukaiCoreSwift/Models/BakingBad/TzKTBalance.swift @@ -21,6 +21,12 @@ public struct TzKTBalance: Codable { /// Details about the Token public let token: TzKTBalanceToken + /// The block level where the token was first seen + public let firstLevel: Decimal + + /// The block level where the token was last seen + public let lastLevel: Decimal + /// Helper to convert the RPC token balance to a `TokenAmount` object public var tokenAmount: TokenAmount { return TokenAmount(fromRpcAmount: balance, decimalPlaces: token.metadata?.decimalsInt ?? 0) ?? .zero() diff --git a/Sources/KukaiCoreSwift/Models/Config/TezosNodeClientConfig.swift b/Sources/KukaiCoreSwift/Models/Config/TezosNodeClientConfig.swift index cb8cc144..891e7efb 100644 --- a/Sources/KukaiCoreSwift/Models/Config/TezosNodeClientConfig.swift +++ b/Sources/KukaiCoreSwift/Models/Config/TezosNodeClientConfig.swift @@ -16,7 +16,8 @@ public struct TezosNodeClientConfig { /// An enum indicating whether the network is mainnet or testnet public enum NetworkType: String { case mainnet - case testnet + case ghostnet + case protocolnet } /// Allow switching between local forging or remote forging+parsing @@ -49,7 +50,7 @@ public struct TezosNodeClientConfig { } /// Preconfigured struct with all the URL's needed to work with Tezos testnet - public struct defaultTestnetURLs { + public struct defaultGhostnetURLs { /// The default testnet URLs to use for estimating and injecting operations public static let nodeURLs = [URL(string: "https://ghostnet.smartpy.io")!, URL(string: "https://rpc.ghostnet.tzboot.net")!] @@ -141,13 +142,16 @@ public struct TezosNodeClientConfig { tezosDomainsURL = TezosNodeClientConfig.defaultMainnetURLs.tezosDomainsURL objktApiURL = TezosNodeClientConfig.defaultMainnetURLs.objktApiURL - case .testnet: - nodeURLs = TezosNodeClientConfig.defaultTestnetURLs.nodeURLs + case .ghostnet: + nodeURLs = TezosNodeClientConfig.defaultGhostnetURLs.nodeURLs forgingType = .local - tzktURL = TezosNodeClientConfig.defaultTestnetURLs.tzktURL - betterCallDevURL = TezosNodeClientConfig.defaultTestnetURLs.betterCallDevURL - tezosDomainsURL = TezosNodeClientConfig.defaultTestnetURLs.tezosDomainsURL - objktApiURL = TezosNodeClientConfig.defaultTestnetURLs.objktApiURL + tzktURL = TezosNodeClientConfig.defaultGhostnetURLs.tzktURL + betterCallDevURL = TezosNodeClientConfig.defaultGhostnetURLs.betterCallDevURL + tezosDomainsURL = TezosNodeClientConfig.defaultGhostnetURLs.tezosDomainsURL + objktApiURL = TezosNodeClientConfig.defaultGhostnetURLs.objktApiURL + + case .protocolnet: + fatalError("No defaults for networkType protocolnet. Must be user supplied") } } diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index 9e74f11b..3b6aee00 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -70,16 +70,17 @@ public class LedgerWallet: Wallet { /** Sign a hex string. - If the string starts with "03" and is not 32 characters long, it will be treated as a watermarked operation and Ledger will be asked to parse + display the operation details. - Else it will be treated as an unknown operation and will simply display the Blake2b hash. - Please be careful when asking the Ledger to parse (passing in an operation), Ledgers have very limited display ability. Keep it to a single operation, not invoking a smart contract + If its an operation "03" will be prefix to the start, if not the hex will be passed directly to the ledger as it now supports parsing strings directly, but requires them to be unhashed */ public func sign(_ hex: String, isOperation: Bool, completion: @escaping ((Result<[UInt8], KukaiError>) -> Void)) { - let isWatermarkedOperation = (String(hex.prefix(2)) == "03") && hex.count != 32 + var hexToSign = hex + if isOperation { + hexToSign = "03"+hex + } LedgerService.shared.connectTo(uuid: ledgerUUID) - .flatMap { _ -> AnyPublisher in - return LedgerService.shared.sign(hex: hex, parse: isWatermarkedOperation) + .flatMap { [weak self] _ -> AnyPublisher in + return LedgerService.shared.sign(hex: hexToSign, forDerivationPath: self?.derivationPath ?? HD.defaultDerivationPath, parse: true) } .sink(onError: { error in completion(Result.failure(error)) @@ -107,6 +108,6 @@ public class LedgerWallet: Wallet { */ public func publicKeyBase58encoded() -> String { let publicKeyData = Data(hexString: publicKey) ?? Data() - return Base58Check.encode(message: publicKeyData.bytes, prefix: Prefix.Keys.Ed25519.public) + return Base58Check.encode(message: publicKeyData.bytes(), prefix: Prefix.Keys.Ed25519.public) } } diff --git a/Sources/KukaiCoreSwift/Models/NFT.swift b/Sources/KukaiCoreSwift/Models/NFT.swift index 955eb239..bdcebee8 100644 --- a/Sources/KukaiCoreSwift/Models/NFT.swift +++ b/Sources/KukaiCoreSwift/Models/NFT.swift @@ -64,6 +64,12 @@ public struct NFT: Codable, Hashable { return favouriteSortIndex != nil } + /// The block level where the token was first seen + public var firstlevel: Decimal + + /// The block level where the token was last seen + public var lastLevel: Decimal + /** Create a more developer friednly `NFT` from a generic `TzKTBalance` object @@ -83,6 +89,8 @@ public struct NFT: Codable, Hashable { displayURI = URL(string: tzkt.token.metadata?.displayUri ?? "") thumbnailURI = URL(string: tzkt.token.metadata?.thumbnailUri ?? "") metadata = tzkt.token.metadata + firstlevel = tzkt.firstLevel + lastLevel = tzkt.lastLevel } /// Confomring to Equatable diff --git a/Sources/KukaiCoreSwift/Models/Token.swift b/Sources/KukaiCoreSwift/Models/Token.swift index 3eee7a52..b529d3c0 100644 --- a/Sources/KukaiCoreSwift/Models/Token.swift +++ b/Sources/KukaiCoreSwift/Models/Token.swift @@ -87,6 +87,12 @@ public class Token: Codable, CustomStringConvertible { /// Recording if the position the index the user chose for the favourite token to appear public var favouriteSortIndex: Int? = nil + /// The block level where the token was first seen + public var firstlevel: Decimal + + /// The block level where the token was last seen + public var lastLevel: Decimal + /// The individual NFT's owned of this token type public var nfts: [NFT]? @@ -126,6 +132,8 @@ public class Token: Codable, CustomStringConvertible { self.tokenId = tokenId self.nfts = nfts self.mintingTool = mintingTool + self.firstlevel = 0 + self.lastLevel = 0 // TODO: make failable init if let faVersion = faVersion, faVersion == .fa2 && tokenId == nil { @@ -136,23 +144,25 @@ public class Token: Codable, CustomStringConvertible { /** Init a `Token` from an object returned by the TzKT API */ - public init(from: TzKTBalanceToken, andTokenAmount: TokenAmount, stakedTokenAmount: TokenAmount? = nil, unstakedTokenAmount: TokenAmount? = nil) { - let decimalsString = from.metadata?.decimals ?? "0" + public init(from: TzKTBalance, andTokenAmount: TokenAmount, stakedTokenAmount: TokenAmount? = nil, unstakedTokenAmount: TokenAmount? = nil) { + let decimalsString = from.token.metadata?.decimals ?? "0" let decimalsInt = Int(decimalsString) ?? 0 - let isNFT = (from.metadata?.artifactUri != nil && decimalsInt == 0 && from.standard == .fa2) + let isNFT = (from.token.metadata?.artifactUri != nil && decimalsInt == 0 && from.token.standard == .fa2) - self.name = from.contract.alias - self.symbol = isNFT ? from.contract.alias ?? "" : from.displaySymbol + self.name = from.token.contract.alias + self.symbol = isNFT ? from.token.contract.alias ?? "" : from.token.displaySymbol self.tokenType = isNFT ? .nonfungible : .fungible - self.faVersion = from.standard + self.faVersion = from.token.standard self.balance = andTokenAmount self.stakedBalance = stakedTokenAmount ?? .zeroBalance(decimalPlaces: andTokenAmount.decimalPlaces) self.unstakedBalance = unstakedTokenAmount ?? .zeroBalance(decimalPlaces: andTokenAmount.decimalPlaces) - self.thumbnailURL = from.metadata?.thumbnailURL ?? TzKTClient.avatarURL(forToken: from.contract.address) - self.tokenContractAddress = from.contract.address - self.tokenId = Decimal(string: from.tokenId) ?? 0 + self.thumbnailURL = from.token.metadata?.thumbnailURL ?? TzKTClient.avatarURL(forToken: from.token.contract.address) + self.tokenContractAddress = from.token.contract.address + self.tokenId = Decimal(string: from.token.tokenId) ?? 0 self.nfts = [] - self.mintingTool = from.metadata?.mintingTool + self.mintingTool = from.token.metadata?.mintingTool + self.firstlevel = from.firstLevel + self.lastLevel = from.lastLevel // TODO: make failable init if let faVersion = faVersion, faVersion == .fa2 && tokenId == nil { @@ -183,6 +193,8 @@ public class Token: Codable, CustomStringConvertible { self.tokenId = Decimal(string: from.token.tokenId) ?? 0 self.nfts = [] self.mintingTool = from.mintingTool + self.firstlevel = from.level + self.lastLevel = from.level // TODO: make failable init if let faVersion = faVersion, faVersion == .fa2 && tokenId == nil { diff --git a/Sources/KukaiCoreSwift/Models/Wallet.swift b/Sources/KukaiCoreSwift/Models/Wallet.swift index fc28ab83..a6732fb5 100644 --- a/Sources/KukaiCoreSwift/Models/Wallet.swift +++ b/Sources/KukaiCoreSwift/Models/Wallet.swift @@ -45,6 +45,7 @@ public protocol Wallet: Codable { /** Sign a hex string with the wallets private key - parameter hex: A hex encoded string, representing a forged operation payload. + - parameter isOperation: A boolean to indicate whether its an operation or something else such as an expression to sign. So that the appropriate prefix can be added automatically - parameter completion: A completion block to run with the resulting signature, needs to be done async in order to support usecases such as signing with an external ledger. */ func sign(_ hex: String, isOperation: Bool, completion: @escaping ((Result<[UInt8], KukaiError>) -> Void)) diff --git a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift index 2f80775d..edfe6ae3 100644 --- a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift +++ b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift @@ -8,7 +8,7 @@ import Foundation /// Container to store groups of WalletMetadata based on type -public struct WalletMetadataList: Codable, Hashable { +public class WalletMetadataList: Codable, Hashable { public var socialWallets: [WalletMetadata] public var hdWallets: [WalletMetadata] public var linearWallets: [WalletMetadata] @@ -66,6 +66,10 @@ public struct WalletMetadataList: Codable, Hashable { for metadata in ledgerWallets { if metadata.address == address { return metadata } + + for childMetadata in metadata.children { + if childMetadata.address == address { return childMetadata } + } } for metaData in watchWallets { @@ -85,7 +89,7 @@ public struct WalletMetadataList: Codable, Hashable { return nil } - public mutating func update(address: String, with newMetadata: WalletMetadata) -> Bool { + public func update(address: String, with newMetadata: WalletMetadata) -> Bool { for (index, metadata) in socialWallets.enumerated() { if metadata.address == address { socialWallets[index] = newMetadata; return true } } @@ -94,7 +98,7 @@ public struct WalletMetadataList: Codable, Hashable { if metadata.address == address { hdWallets[index] = newMetadata; return true } for (childIndex, childMetadata) in metadata.children.enumerated() { - if childMetadata.address == address { hdWallets[index].children[childIndex] = newMetadata; return true } + if childMetadata.address == address { hdWallets[index].children[childIndex] = newMetadata; return true } } } @@ -104,6 +108,10 @@ public struct WalletMetadataList: Codable, Hashable { for (index, metadata) in ledgerWallets.enumerated() { if metadata.address == address { ledgerWallets[index] = newMetadata; return true } + + for (childIndex, childMetadata) in metadata.children.enumerated() { + if childMetadata.address == address { ledgerWallets[index].children[childIndex] = newMetadata; return true } + } } for (index, metadata) in watchWallets.enumerated() { @@ -113,8 +121,8 @@ public struct WalletMetadataList: Codable, Hashable { return false } - public mutating func set(mainnetDomain: TezosDomainsReverseRecord?, ghostnetDomain: TezosDomainsReverseRecord?, forAddress address: String) -> Bool { - var meta = metadata(forAddress: address) + public func set(mainnetDomain: TezosDomainsReverseRecord?, ghostnetDomain: TezosDomainsReverseRecord?, forAddress address: String) -> Bool { + let meta = metadata(forAddress: address) if let mainnet = mainnetDomain { meta?.mainnetDomains = [mainnet] @@ -131,8 +139,8 @@ public struct WalletMetadataList: Codable, Hashable { return false } - public mutating func set(nickname: String?, forAddress address: String) -> Bool { - var meta = metadata(forAddress: address) + public func set(nickname: String?, forAddress address: String) -> Bool { + let meta = metadata(forAddress: address) meta?.walletNickname = nickname if let meta = meta, update(address: address, with: meta) { @@ -142,8 +150,8 @@ public struct WalletMetadataList: Codable, Hashable { return false } - public mutating func set(hdWalletGroupName: String, forAddress address: String) -> Bool { - var meta = metadata(forAddress: address) + public func set(hdWalletGroupName: String, forAddress address: String) -> Bool { + let meta = metadata(forAddress: address) meta?.hdWalletGroupName = hdWalletGroupName if let meta = meta, update(address: address, with: meta) { @@ -154,12 +162,16 @@ public struct WalletMetadataList: Codable, Hashable { } public func count() -> Int { - var total = (socialWallets.count + linearWallets.count + ledgerWallets.count + watchWallets.count) + var total = (socialWallets.count + linearWallets.count + watchWallets.count) for wallet in hdWallets { total += (1 + wallet.children.count) } + for wallet in ledgerWallets { + total += (1 + wallet.children.count) + } + return total } @@ -184,6 +196,10 @@ public struct WalletMetadataList: Codable, Hashable { for metadata in ledgerWallets { temp.append(metadata.address) + + for childMetadata in metadata.children { + temp.append(childMetadata.address) + } } for metadata in watchWallets { @@ -224,6 +240,22 @@ public struct WalletMetadataList: Codable, Hashable { return temp } + + public static func == (lhs: WalletMetadataList, rhs: WalletMetadataList) -> Bool { + return lhs.socialWallets == rhs.socialWallets && + lhs.hdWallets == rhs.hdWallets && + lhs.linearWallets == rhs.linearWallets && + lhs.ledgerWallets == rhs.ledgerWallets && + lhs.watchWallets == rhs.watchWallets + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(socialWallets) + hasher.combine(hdWallets) + hasher.combine(linearWallets) + hasher.combine(ledgerWallets) + hasher.combine(watchWallets) + } } @@ -231,8 +263,9 @@ public struct WalletMetadataList: Codable, Hashable { /// Object to store UI related info about wallets, seperated from the wallet object itself to avoid issues merging together -public struct WalletMetadata: Codable, Hashable { +public class WalletMetadata: Codable, Hashable { public var address: String + public var derivationPath: String? public var hdWalletGroupName: String? public var walletNickname: String? public var socialUsername: String? @@ -287,8 +320,9 @@ public struct WalletMetadata: Codable, Hashable { } } - public init(address: String, hdWalletGroupName: String?, walletNickname: String? = nil, socialUsername: String? = nil, socialUserId: String? = nil, mainnetDomains: [TezosDomainsReverseRecord]? = nil, ghostnetDomains: [TezosDomainsReverseRecord]? = nil, socialType: TorusAuthProvider? = nil, type: WalletType, children: [WalletMetadata], isChild: Bool, isWatchOnly: Bool, bas58EncodedPublicKey: String, backedUp: Bool) { + public init(address: String, derivationPath: String?, hdWalletGroupName: String?, walletNickname: String? = nil, socialUsername: String? = nil, socialUserId: String? = nil, mainnetDomains: [TezosDomainsReverseRecord]? = nil, ghostnetDomains: [TezosDomainsReverseRecord]? = nil, socialType: TorusAuthProvider? = nil, type: WalletType, children: [WalletMetadata], isChild: Bool, isWatchOnly: Bool, bas58EncodedPublicKey: String, backedUp: Bool) { self.address = address + self.derivationPath = derivationPath self.hdWalletGroupName = hdWalletGroupName self.walletNickname = walletNickname self.socialUsername = socialUsername diff --git a/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift b/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift index a8b42821..f7359edb 100644 --- a/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift +++ b/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift @@ -128,7 +128,7 @@ public struct KukaiError: CustomStringConvertible, Error { switch errorType { case .rpc: if let rpcErrorString = rpcErrorString { - return "RPC: \(rpcErrorString.removeLeadingProtocolFromRPCError() ?? rpcErrorString)" + return rpcErrorString } return "RPC: Unknown" @@ -151,7 +151,7 @@ public struct KukaiError: CustomStringConvertible, Error { case .internalApplication: if let subType = subType { - return "Internal Application Error: \(subType.localizedDescription)" + return "Internal Application Error: \(subType)" } return "Internal Application Error: Unknown" @@ -238,9 +238,13 @@ public class ErrorHandlingService { /// Convert an `OperationResponseInternalResultError` into a `KukaiError` and optionally log it to the central logger public static func fromOperationError(_ opError: OperationResponseInternalResultError, requestURL: URL?, andLog: Bool = true) -> KukaiError { let errorWithoutProtocol = opError.id.removeLeadingProtocolFromRPCError() - var errorToReturn = KukaiError(errorType: .rpc, knownErrorMessage: nil, subType: nil, rpcErrorString: errorWithoutProtocol, failWith: nil, requestURL: nil, requestJSON: nil, responseJSON: nil, httpStatusCode: nil) + var errorToReturn = KukaiError(errorType: .rpc, knownErrorMessage: nil, subType: nil, rpcErrorString: "RPC error code: \(errorWithoutProtocol ?? "Unknown")", failWith: nil, requestURL: nil, requestJSON: nil, responseJSON: nil, httpStatusCode: nil) - if (errorWithoutProtocol == "michelson_v1.runtime_error" || errorWithoutProtocol == "michelson_v1.script_rejected"), let withError = opError.with { + + if let result = knownRPCErrorString(rpcStringWithoutLeadingProtocol: errorWithoutProtocol, with: opError.with) { + errorToReturn = KukaiError.rpcError(rpcErrorString: result, andFailWith: opError.with, requestURL: requestURL) + + } else if (errorWithoutProtocol == "michelson_v1.runtime_error" || errorWithoutProtocol == "michelson_v1.script_rejected"), let withError = opError.with { if let failwith = withError.int, let failwithInt = Int(failwith) { // Smart contract failwith reached with an Int denoting an error code @@ -265,6 +269,56 @@ public class ErrorHandlingService { return errorToReturn } + public static func knownRPCErrorString(rpcStringWithoutLeadingProtocol: String?, with: FailWith?) -> String? { + guard let rpcStringWithoutLeadingProtocol = rpcStringWithoutLeadingProtocol else { return nil } + + switch rpcStringWithoutLeadingProtocol { + + // top level protocol errors + case "implicit.empty_implicit_contract": + return "Your account has a 0 XTZ balance and is unable to pay for the fees to complete the transaction." + + case "tez.subtraction_underflow": + return "Your account does not have enough XTZ to complete the transaction." + + + // known michelson contract related errors (e.g. insufficent balance for a known token standard) + // can appear in 2 places + case "michelson_v1.script_rejected": + if let string = with?.string { + return compareInnerString(string) + + } else if let string = with?.args?.first?["string"] { + return compareInnerString(string) + + } else { + return nil + } + + default: + return nil + } + } + + private static func compareInnerString(_ remoteString: String?) -> String? { + switch remoteString?.lowercased() { + case "fa2_insufficient_balance": + return "Insufficient NFT/DeFi token balance to complete the transaction" + + case "fa1.2_insufficientbalance": + return "Insufficient DeFi token balance to complete the transaction" + + case "notenoughbalance": + return "Insufficient token balance to complete the transaction" + + case .none: + return nil + + case .some(_): + return nil + } + } + /// Search an `OperationResponse` to see does it contain any errors, if so return the last one as a `KukaiError` and optionally log it to the central logger public static func searchOperationResponseForErrors(_ opResponse: OperationResponse, requestURL: URL?, andLog: Bool = true) -> KukaiError? { if let lastError = opResponse.errors().last { @@ -311,7 +365,8 @@ public class ErrorHandlingService { let lastError = errorArray.last, let errorString = lastError.id.removeLeadingProtocolFromRPCError() { - errorToReturn = KukaiError.rpcError(rpcErrorString: errorString, andFailWith: lastError.with, requestURL: requestURL) + let updatedString = knownRPCErrorString(rpcStringWithoutLeadingProtocol: errorString, with: lastError.with) + errorToReturn = KukaiError.rpcError(rpcErrorString: updatedString ?? "RPC error code: \(errorString)", andFailWith: lastError.with, requestURL: requestURL) } else { errorToReturn.addNetworkData(requestURL: requestURL, requestJSON: requestData, responseJSON: data, httpStatusCode: httpResponse.statusCode) } diff --git a/Sources/KukaiCoreSwift/Services/LedgerService.swift b/Sources/KukaiCoreSwift/Services/LedgerService.swift index 03b6fcaa..b468d45b 100644 --- a/Sources/KukaiCoreSwift/Services/LedgerService.swift +++ b/Sources/KukaiCoreSwift/Services/LedgerService.swift @@ -77,6 +77,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele case LICENSING = "6f42" case HALTED = "6faa" + case APP_CLOSED = "6e01" case DEVICE_LOCKED = "009000" case UNKNOWN = "99999999" case NO_WRITE_CHARACTERISTIC = "99999996" @@ -143,6 +144,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele private var bag_connection = Set() private var bag_writer = Set() private var bag_apdu = Set() + private var bag_addressFetcher = Set() private var counter = 0 /// Public shared instace to avoid having multiple copies of the underlying `JSContext` created @@ -184,7 +186,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele // Convert the supplied data into an APDU. Returns a single string per ADPU, but broken up into chunks, seperated by spaces for each maximum sized data packet - guard let sendAPDU = self?.jsContext.evaluateScript("ledger_app_tezos.sendAPDU(\"\(apdu)\", 156)").toString() else { + guard let sendAPDU = self?.jsContext.evaluateScript("ledger_app_tezos.sendAPDU(\"\(apdu)\", 20)").toString() else { self?.deviceConnectedPublisher.send(false) return } @@ -255,6 +257,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele */ public func stopListening() { self.centralManager?.stopScan() + self.deviceList = [:] self.deviceListPublisher.send(completion: .finished) } @@ -263,14 +266,14 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele - returns: Publisher which will indicate true / false, or return an `KukaiError` if it can't connect to bluetooth */ public func connectTo(uuid: String) -> AnyPublisher { - if self.connectedDevice != nil, self.connectedDevice?.identifier.uuidString == uuid { + if self.connectedDevice != nil, self.connectedDevice?.identifier.uuidString == uuid, self.connectedDevice?.state == .connected { return AnyPublisher.just(true) } self.setupBluetoothConnection() .sink { [weak self] value in if !value { - self?.deviceConnectedPublisher.send(completion: .failure(KukaiError.unknown())) + self?.deviceConnectedPublisher.send(false) } self?.requestedUUID = uuid @@ -290,6 +293,8 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele if let device = self.connectedDevice { self.centralManager?.cancelPeripheralConnection(device) } + + requestedUUID = nil } /** @@ -297,7 +302,11 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele - returns: a string if it can be found */ public func getConnectedDeviceUUID() -> String? { - return self.connectedDevice?.identifier.uuidString + if self.connectedDevice?.state == .connected { + return self.connectedDevice?.identifier.uuidString + } else { + return nil + } } /** @@ -329,6 +338,29 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele }).eraseToAnyPublisher() } + /** + Get a TZ address and public key from the current connected Ledger device + - parameter forDerivationPath: Optional. The derivation path to use to extract the address from the underlying HD wallet + - parameter curve: Optional. The `EllipticalCurve` to use to extract the address + - parameter verify: Whether or not to ask the ledger device to prompt the user to show them what the TZ address should be, to ensure the mobile matches + - returns: An async `Result` object, allowing code to be triggered via while loops more easily + */ + public func getAddress(forDerivationPath derivationPath: String = HD.defaultDerivationPath, curve: EllipticalCurve = .ed25519, verify: Bool) async -> Result<(address: String, publicKey: String), KukaiError> { + return await withCheckedContinuation({ continuation in + var cancellable: AnyCancellable! + cancellable = getAddress(forDerivationPath: derivationPath, curve: curve, verify: verify) + .sink(onError: { error in + continuation.resume(returning: Result.failure(error)) + }, onSuccess: { addressObj in + continuation.resume(returning: Result.success(addressObj)) + }, onComplete: { [weak self] in + self?.bag_addressFetcher.remove(cancellable) + }) + + cancellable.store(in: &bag_addressFetcher) + }) + } + /** Sign an operation payload with the underlying secret key, returning the signature - parameter hex: An operation converted to JSON, forged and watermarked, converted to a hex string. (Note: there are some issues with the ledger app signing batch transactions. May simply return no result at all. Can't run REVEAL and TRANSACTION together for example) @@ -396,6 +428,9 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { Logger.ledger.info("Failed to connect to \(peripheral.name ?? ""), \(peripheral.identifier.uuidString)") self.connectedDevice = nil + self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil self.deviceConnectedPublisher.send(false) } @@ -404,6 +439,9 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele guard let services = peripheral.services else { Logger.ledger.info("Unable to locate services for: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil + self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil self.deviceConnectedPublisher.send(false) return } @@ -416,11 +454,32 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele } } + public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: (any Error)?) { + Logger.ledger.info("Disconnected: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") + self.connectedDevice = nil + self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil + self.deviceConnectedPublisher.send(false) + } + + public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, timestamp: CFAbsoluteTime, isReconnecting: Bool, error: (any Error)?) { + Logger.ledger.info("Disconnected: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") + self.connectedDevice = nil + self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil + self.deviceConnectedPublisher.send(false) + } + /// CBCentralManagerDelegate function, must be marked public because of protocol definition public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { guard let characteristics = service.characteristics else { Logger.ledger.info("Unable to locate characteristics for: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil + self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil self.deviceConnectedPublisher.send(false) return } diff --git a/Sources/KukaiCoreSwift/Services/MediaProxyService.swift b/Sources/KukaiCoreSwift/Services/MediaProxyService.swift index 39e024be..8c980c64 100644 --- a/Sources/KukaiCoreSwift/Services/MediaProxyService.swift +++ b/Sources/KukaiCoreSwift/Services/MediaProxyService.swift @@ -96,7 +96,7 @@ public class MediaProxyService: NSObject { public static func setupImageLibrary() { MediaProxyService.permanentCache.config.maxMemoryCost = UInt(100 * 1000 * 1000) // 100 MB - MediaProxyService.temporaryCache.config.maxDiskAge = 3600 * 24 * 7 // 1 Week + MediaProxyService.temporaryCache.config.maxDiskAge = 3600 * 24 * 30 // 30 days MediaProxyService.temporaryCache.config.maxMemoryCost = UInt(500 * 1000 * 1000) // 500 MB MediaProxyService.detailCache.config.maxDiskAge = 3600 * 24 // 1 day diff --git a/Sources/KukaiCoreSwift/Services/NetworkService.swift b/Sources/KukaiCoreSwift/Services/NetworkService.swift index 3fabf405..f51fd5d9 100644 --- a/Sources/KukaiCoreSwift/Services/NetworkService.swift +++ b/Sources/KukaiCoreSwift/Services/NetworkService.swift @@ -216,6 +216,42 @@ public class NetworkService { }.eraseToAnyPublisher() } + /** + Send a HTTP DELETE to a given URL + */ + public func delete(url: URL, completion: @escaping ((Result) -> Void)) { + var request = URLRequest(url: url) + request.addValue("application/json", forHTTPHeaderField: "Accept") + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = "DELETE" + + urlSession.dataTask(with: request) { (data, response, error) in + if let err = error { + completion(Result.failure(KukaiError.internalApplicationError(error: err))) + } else { + completion(Result.success(true)) + } + }.resume() + NetworkService.logRequestStart(loggingConfig: loggingConfig, fullURL: url) + } + + /** + Send a HTTP DELETE to a given URL + */ + public func delete(url: URL) -> AnyPublisher { + return Future { [weak self] promise in + self?.delete(url: url, completion: { result in + guard let output = try? result.get() else { + let error = (try? result.getError()) ?? KukaiError.unknown() + promise(.failure(error)) + return + } + + promise(.success(output)) + }) + }.eraseToAnyPublisher() + } + func checkForRPCOperationErrors(parsedResponse: Any, withRequestURL: URL?, requestPayload: Data?, responsePayload: Data?, httpStatusCode: Int?) -> KukaiError? { var operations: [OperationResponse] = [] diff --git a/Sources/KukaiCoreSwift/Services/OperationService.swift b/Sources/KukaiCoreSwift/Services/OperationService.swift index 0f827b88..324338f5 100644 --- a/Sources/KukaiCoreSwift/Services/OperationService.swift +++ b/Sources/KukaiCoreSwift/Services/OperationService.swift @@ -123,15 +123,9 @@ public class OperationService { /// Internal function to group together operations for readability sake private func signPreapplyAndInject(wallet: Wallet, forgedHash: String, operationPayload: OperationPayload, operationMetadata: OperationMetadata, completion: @escaping ((Result) -> Void)) { - var stringToSign = forgedHash - - if wallet.type == .ledger { - stringToSign = ledgerStringToSign(forgedHash: forgedHash, operationPayload: operationPayload) - } - - + // Sign whatever string is required, and move on to preapply / inject - wallet.sign(stringToSign, isOperation: true) { [weak self] result in + wallet.sign(forgedHash, isOperation: true) { [weak self] result in guard let signature = try? result.get() else { completion(Result.failure(result.getFailure())) return @@ -141,28 +135,6 @@ public class OperationService { } } - /** - Ledger can only parse operations under certain conditions. These conditions are not documented well. This function will attempt to determine whether the payload can be parsed or not, and returnt he appropriate string for the LedgerWallet sign function - It seems to be able to parse the payload if it contains 1 operation, of the below types. Combining types (like Reveal + Transation) causes a parse error - If the payload structure passes the conditions we are aware of, allow parsing to take place. If not, sign blake2b hash instead - */ - public func ledgerStringToSign(forgedHash: String, operationPayload: OperationPayload) -> String { - let watermarkedOp = "03" + forgedHash - let watermarkedBytes = Sodium.shared.utils.hex2bin(watermarkedOp) ?? [] - let blakeHash = Sodium.shared.genericHash.hash(message: watermarkedBytes, outputLength: 32) - let blakeHashString = blakeHash?.toHexString() ?? "" - - // Ledger can only parse operations under certain conditions. These conditions are not documented well. - // It seems to be able to parse the payload if it contains 1 operation, of the below types. Combining types (like Reveal + Transation) causes a parse error - // If the payload structure passes the conditions we are aware of, allow parsing to take place. If not, sign blake2b hash instead - var ledgerCanParse = false - if operationPayload.contents.count == 1, let first = operationPayload.contents.first, (first is OperationReveal || first is OperationDelegation || first is OperationTransaction) { - ledgerCanParse = true - } - - return (ledgerCanParse ? watermarkedOp : blakeHashString) - } - /** Preapply and Inject wrapped up as one function, for situations like Ledger Wallets, where signing is a complately different process, and must be done elsewhere - parameter forgedOperation: The forged operation hex without a watermark. diff --git a/Sources/KukaiCoreSwift/Services/TorusAuthService.swift b/Sources/KukaiCoreSwift/Services/TorusAuthService.swift index 9704d11f..0390ca85 100644 --- a/Sources/KukaiCoreSwift/Services/TorusAuthService.swift +++ b/Sources/KukaiCoreSwift/Services/TorusAuthService.swift @@ -161,10 +161,10 @@ public class TorusAuthService: NSObject { aggregateVerifierType: verifierWrapper.verifierType, aggregateVerifier: verifierWrapper.aggregateVerifierName ?? verifierWrapper.subverifier.verifier, subVerifierDetails: [verifierWrapper.subverifier], - network: verifierWrapper.networkType == .testnet ? .legacy(.TESTNET) : .legacy(.MAINNET), + network: verifierWrapper.networkType == .mainnet ? .legacy(.MAINNET) : .legacy(.TESTNET), loglevel: .error, urlSession: self.networkService.urlSession, - networkUrl: verifierWrapper.networkType == .testnet ? "https://www.ankr.com/rpc/eth/eth_goerli" : nil) + networkUrl: verifierWrapper.networkType == .mainnet ? nil : "https://www.ankr.com/rpc/eth/eth_goerli") } @@ -250,21 +250,22 @@ public class TorusAuthService: NSObject { completion(Result.failure(KukaiError.internalApplicationError(error: TorusAuthError.missingVerifier))) } + let accessToken = data.userInfo["access_token"] as? String // Twitter API doesn't give us the bloody "@" handle for some reason. Fetch that first and overwrite the username property with the handle, if found if authType == .twitter { twitterHandleLookup(id: userId ?? "") { [weak self] result in switch result { case .success(let actualUsername): - self?.createTorusWalletAndContinue(pk: pk, authType: authType, username: actualUsername, userId: userId, profile: profile, completion: completion) + self?.createTorusWalletAndContinue(pk: pk, authType: authType, username: actualUsername, userId: userId, profile: profile, accessToken: accessToken, completion: completion) case .failure(_): - self?.createTorusWalletAndContinue(pk: pk, authType: authType, username: username, userId: userId, profile: profile, completion: completion) + self?.createTorusWalletAndContinue(pk: pk, authType: authType, username: username, userId: userId, profile: profile, accessToken: accessToken, completion: completion) } } } else { - createTorusWalletAndContinue(pk: pk, authType: authType, username: username, userId: userId, profile: profile, completion: completion) + createTorusWalletAndContinue(pk: pk, authType: authType, username: username, userId: userId, profile: profile, accessToken: accessToken, completion: completion) } } catch { @@ -275,14 +276,20 @@ public class TorusAuthService: NSObject { } } - private func createTorusWalletAndContinue(pk: String?, authType: TorusAuthProvider, username: String?, userId: String?, profile: String?, completion: @escaping ((Result) -> Void)) { + private func createTorusWalletAndContinue(pk: String?, authType: TorusAuthProvider, username: String?, userId: String?, profile: String?, accessToken: String?, completion: @escaping ((Result) -> Void)) { guard let privateKeyString = pk, let wallet = TorusWallet(authProvider: authType, username: username, userId: userId, profilePicture: profile, torusPrivateKey: privateKeyString) else { Logger.torus.error("Error torus contained no, or invlaid private key") completion(Result.failure(KukaiError.internalApplicationError(error: TorusAuthError.invalidTorusResponse))) return } - completion(Result.success(wallet)) + if authType == .facebook, let token = accessToken, let url = URL(string: "https://graph.facebook.com/me/permissions?access_token=\(token)") { + networkService.delete(url: url) { result in + completion(Result.success(wallet)) + } + } else { + completion(Result.success(wallet)) + } } @@ -317,7 +324,7 @@ public class TorusAuthService: NSObject { /// Private wrapper to avoid duplication in the previous function private func getPublicAddress(verifierName: String, verifierWrapper: SubverifierWrapper, socialUserId: String, completion: @escaping ((Result) -> Void)) { - let isTestnet = (verifierWrapper.networkType == .testnet) + let isTestnet = (verifierWrapper.networkType != .mainnet) self.fetchNodeDetails = NodeDetailManager(network: (isTestnet ? .legacy(.TESTNET) : .legacy(.MAINNET)), urlSession: networkService.urlSession) Task { @@ -457,10 +464,10 @@ extension TorusAuthService: ASAuthorizationControllerDelegate, ASAuthorizationCo aggregateVerifierType: verifierWrapper.verifierType, aggregateVerifier: verifierWrapper.aggregateVerifierName ?? verifierWrapper.subverifier.verifier, subVerifierDetails: [verifierWrapper.subverifier], - network: verifierWrapper.networkType == .testnet ? .legacy(.TESTNET) : .legacy(.MAINNET), + network: verifierWrapper.networkType == .mainnet ? .legacy(.MAINNET) : .legacy(.TESTNET), loglevel: .error, urlSession: self.networkService.urlSession, - networkUrl: verifierWrapper.networkType == .testnet ? "https://www.ankr.com/rpc/eth/eth_goerli" : nil) + networkUrl: verifierWrapper.networkType == .mainnet ? nil : "https://www.ankr.com/rpc/eth/eth_goerli") Task { @MainActor in do { diff --git a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift index 65e7f9fd..77f871c0 100644 --- a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift +++ b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift @@ -99,52 +99,117 @@ public class WalletCacheService { var newWallets = existingWallets newWallets[wallet.address] = wallet - var newMetadata = readMetadataFromDiskAndDecrypt() + let newMetadata = readMetadataFromDiskAndDecrypt() + var array = metadataArray(forType: wallet.type, fromMeta: newMetadata) + + var path: String? = nil + if wallet.type == .hd, let w = wallet as? HDWallet { + path = w.derivationPath + } else if wallet.type == .ledger, let w = wallet as? LedgerWallet { + path = w.derivationPath + } + if let index = childOfIndex { - if index >= newMetadata.hdWallets.count { - Logger.walletCache.error("WalletCacheService metadata insertion issue. Requested to add to HDWallet at index \"\(index)\", when there are currently only \"\(newMetadata.hdWallets.count)\" items") + + // If child index is present, update the correct sub array to include this new item, checking forst that we have the correct details + if index >= array.count { + Logger.walletCache.error("WalletCacheService metadata insertion issue. Requested to add at index \"\(index)\", when there are currently only \"\(array.count)\" items") throw WalletCacheError.requestedIndexTooHigh } - newMetadata.hdWallets[index].children.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, type: wallet.type, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) + array[index].children.append(WalletMetadata(address: wallet.address, derivationPath: path, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, type: wallet.type, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) + + } else if wallet.type == .hd || wallet.type == .ledger { - } else if let _ = wallet as? HDWallet { + // If its HD or Ledger (also a HD), these wallets display grouped together with a custom name. Compute the new default name based off existing data and then add + var groupNameStart = "" + switch wallet.type { + case .hd: + groupNameStart = "HD Wallet " + case .ledger: + groupNameStart = "Ledger Device " + case .social, .regular, .regularShifted: + groupNameStart = "" + } var newNumber = 0 - if let lastDefaultName = newMetadata.hdWallets.reversed().first(where: { $0.hdWalletGroupName?.prefix(10) == "HD Wallet " }) { - let numberOnly = lastDefaultName.hdWalletGroupName?.replacingOccurrences(of: "HD Wallet ", with: "") + if let lastDefaultName = array.reversed().first(where: { $0.hdWalletGroupName?.prefix(groupNameStart.count) ?? " " == groupNameStart }) { + let numberOnly = lastDefaultName.hdWalletGroupName?.replacingOccurrences(of: groupNameStart, with: "") newNumber = (Int(numberOnly ?? "0") ?? 0) + 1 } if newNumber == 0 { - newNumber = newMetadata.hdWallets.count + 1 + newNumber = array.count + 1 } - newMetadata.hdWallets.append(WalletMetadata(address: wallet.address, hdWalletGroupName: "HD Wallet \(newNumber)", walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) + array.append(WalletMetadata(address: wallet.address, derivationPath: path, hdWalletGroupName: "\(groupNameStart)\(newNumber)", walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) } else if let torusWallet = wallet as? TorusWallet { - newMetadata.socialWallets.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: torusWallet.socialUsername, socialUserId: torusWallet.socialUserId, socialType: torusWallet.authProvider, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) - } else if let _ = wallet as? LedgerWallet { - newMetadata.ledgerWallets.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) + // If social, cast and fetch special attributes + array.append(WalletMetadata(address: wallet.address, derivationPath: path, hdWalletGroupName: nil, walletNickname: nil, socialUsername: torusWallet.socialUsername, socialUserId: torusWallet.socialUserId, socialType: torusWallet.authProvider, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) } else { - newMetadata.linearWallets.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) + + // Else, add basic wallet to the list its supposed to go to + array.append(WalletMetadata(address: wallet.address, derivationPath: path, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) } - + // Update wallet metadata array, and then commit to disk + updateMetadataArray(forType: wallet.type, withNewArray: array, forMeta: newMetadata) if encryptAndWriteWalletsToDisk(wallets: newWallets) && encryptAndWriteMetadataToDisk(newMetadata) == false { throw WalletCacheError.unableToEncryptAndWrite + } else { + removeNewAddressFromWatchListIfExists(wallet.address, list: newMetadata) } } + + /// Helper method to return the appropriate sub array for the type, to reduce code compelxity + private func metadataArray(forType: WalletType, fromMeta: WalletMetadataList) -> [WalletMetadata] { + switch forType { + case .regular: + return fromMeta.linearWallets + case .regularShifted: + return fromMeta.linearWallets + case .hd: + return fromMeta.hdWallets + case .social: + return fromMeta.socialWallets + case .ledger: + return fromMeta.ledgerWallets + } + } + + /// Helper method to take ina new sub array and update and existing reference, to reduce code complexity + private func updateMetadataArray(forType: WalletType, withNewArray: [WalletMetadata], forMeta: WalletMetadataList) { + switch forType { + case .regular: + forMeta.linearWallets = withNewArray + case .regularShifted: + forMeta.linearWallets = withNewArray + case .hd: + forMeta.hdWallets = withNewArray + case .social: + forMeta.socialWallets = withNewArray + case .ledger: + forMeta.ledgerWallets = withNewArray + } + } + + private func removeNewAddressFromWatchListIfExists(_ address: String, list: WalletMetadataList) { + if let _ = list.watchWallets.first(where: { $0.address == address }) { + let _ = deleteWatchWallet(address: address) + } + } + /** Cahce a watch wallet metadata obj, only. Metadata cahcing handled via wallet cache method */ public func cacheWatchWallet(metadata: WalletMetadata) throws { - var list = readMetadataFromDiskAndDecrypt() + let list = readMetadataFromDiskAndDecrypt() - if let _ = list.watchWallets.first(where: { $0.address == metadata.address }) { - Logger.walletCache.error("cacheWatchWallet - Unable to cache wallet, walelt already exists") + if let _ = list.addresses().first(where: { $0 == metadata.address }) { + Logger.walletCache.error("cacheWatchWallet - Unable to cache wallet, wallet already exists") throw WalletCacheError.walletAlreadyExists } @@ -168,37 +233,31 @@ public class WalletCacheService { } var newWallets = existingWallets + let type = existingWallets[withAddress]?.type ?? .hd newWallets.removeValue(forKey: withAddress) - var newMetadata = readMetadataFromDiskAndDecrypt() + let newMetadata = readMetadataFromDiskAndDecrypt() + var array = metadataArray(forType: type, fromMeta: newMetadata) + if let hdWalletIndex = parentIndex { - guard hdWalletIndex < newMetadata.hdWallets.count, let childIndex = newMetadata.hdWallets[hdWalletIndex].children.firstIndex(where: { $0.address == withAddress }) else { + guard hdWalletIndex < array.count, let childIndex = array[hdWalletIndex].children.firstIndex(where: { $0.address == withAddress }) else { Logger.walletCache.error("Unable to locate wallet") return false } - let _ = newMetadata.hdWallets[hdWalletIndex].children.remove(at: childIndex) + let _ = array[hdWalletIndex].children.remove(at: childIndex) } else { - if let index = newMetadata.hdWallets.firstIndex(where: { $0.address == withAddress }) { + if let index = array.firstIndex(where: { $0.address == withAddress }) { // Children will be removed from metadata automatically, as they are contained inside the parent, however they won't from the encrypted cache // Remove them from encrypted first, then parent from metadata - let children = newMetadata.hdWallets[index].children + let children = array[index].children for child in children { newWallets.removeValue(forKey: child.address) } - let _ = newMetadata.hdWallets.remove(at: index) - - } else if let index = newMetadata.socialWallets.firstIndex(where: { $0.address == withAddress }) { - let _ = newMetadata.socialWallets.remove(at: index) - - } else if let index = newMetadata.linearWallets.firstIndex(where: { $0.address == withAddress }) { - let _ = newMetadata.linearWallets.remove(at: index) - - } else if let index = newMetadata.ledgerWallets.firstIndex(where: { $0.address == withAddress }) { - let _ = newMetadata.ledgerWallets.remove(at: index) + let _ = array.remove(at: index) } else { Logger.walletCache.error("Unable to locate wallet") @@ -206,6 +265,7 @@ public class WalletCacheService { } } + updateMetadataArray(forType: type, withNewArray: array, forMeta: newMetadata) return encryptAndWriteWalletsToDisk(wallets: newWallets) && encryptAndWriteMetadataToDisk(newMetadata) } @@ -213,7 +273,7 @@ public class WalletCacheService { Clear a watch wallet meatadata obj from the metadata cache only, does not affect actual wallet cache */ public func deleteWatchWallet(address: String) -> Bool { - var list = readMetadataFromDiskAndDecrypt() + let list = readMetadataFromDiskAndDecrypt() list.watchWallets.removeAll(where: { $0.address == address }) return encryptAndWriteMetadataToDisk(list) @@ -232,6 +292,29 @@ public class WalletCacheService { return cacheItems[address] } + /** + Migrate a LedgerWallet and its children to a new physical device, denoted by a new UUID + */ + public func migrateLedger(metadata: WalletMetadata, toNewUUID: String) -> Bool { + guard let cacheItems = readWalletsFromDiskAndDecrypt() else { + Logger.walletCache.error("Unable to read wallet items") + return false + } + + var addressesToMigrate = [metadata.address] + addressesToMigrate.append(contentsOf: metadata.children.map({ $0.address })) + + for address in addressesToMigrate { + guard let ledgerWallet = cacheItems[address] as? LedgerWallet else { + return false + } + + ledgerWallet.ledgerUUID = toNewUUID + } + + return encryptAndWriteWalletsToDisk(wallets: cacheItems) + } + /** Delete the cached files and the assoicate keys used to encrypt it - Returns: Bool, indicating if the process was successful or not diff --git a/Tests/KukaiCoreSwiftTests/Clients/TezosNodeClientTests.swift b/Tests/KukaiCoreSwiftTests/Clients/TezosNodeClientTests.swift index 29ee94c0..f09bc2c4 100644 --- a/Tests/KukaiCoreSwiftTests/Clients/TezosNodeClientTests.swift +++ b/Tests/KukaiCoreSwiftTests/Clients/TezosNodeClientTests.swift @@ -101,7 +101,7 @@ class TezosNodeClientTests: XCTestCase { XCTFail("Should have failed, got opHash instead") case .failure(let error): - XCTAssert(error.description == "RPC: contract.counter_in_the_future", error.description) + XCTAssert(error.description == "RPC error code: contract.counter_in_the_future", error.description) } expectation.fulfill() diff --git a/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift b/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift index fcf063b4..87c22b16 100644 --- a/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift +++ b/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift @@ -434,6 +434,62 @@ class TzKTClientTests: XCTestCase { wait(for: [expectation], timeout: 120) } + func testEstimateRewardsNoPrevious() { + let expectation = XCTestExpectation(description: "tzkt-testEstimateRewardsNoPrevious") + let delegate = TzKTAccountDelegate(alias: "The Shire", address: "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv", active: true) + + MockConstants.shared.tzktClient.estimateLastAndNextReward(forAddress: "tz1iv8r8UUCEZK5gqpLPnMPzP4VRJBJUdGgr", delegate: delegate) { result in + switch result { + case .success(let rewards): + XCTAssert(rewards.previousReward == nil, rewards.previousReward?.amount.description ?? "") + + XCTAssert(rewards.estimatedPreviousReward == nil, rewards.estimatedPreviousReward?.amount.normalisedRepresentation ?? "") + + XCTAssert(rewards.estimatedNextReward?.amount.normalisedRepresentation == "0.000368", rewards.estimatedNextReward?.amount.normalisedRepresentation ?? "") + XCTAssert(rewards.estimatedNextReward?.fee.description == "0.042", rewards.estimatedNextReward?.fee.description ?? "") + XCTAssert(rewards.estimatedNextReward?.cycle.description == "743", rewards.estimatedNextReward?.cycle.description ?? "") + XCTAssert(rewards.estimatedNextReward?.bakerAlias == "The Shire", rewards.estimatedNextReward?.bakerAlias ?? "") + + XCTAssert(rewards.moreThan1CycleBetweenPreiousAndNext() == false) + + case .failure(let error): + XCTFail("Error: \(error)") + } + + expectation.fulfill() + } + + wait(for: [expectation], timeout: 120) + } + + func testEstimateRewardsNone() { + let expectation = XCTestExpectation(description: "tzkt-testEstimateRewardsNone") + let delegate = TzKTAccountDelegate(alias: "Teztillery", address: "tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4", active: true) + + MockConstants.shared.tzktClient.estimateLastAndNextReward(forAddress: "tz1ckwbvP7pdTLS1aAe6YPoiKpG2d8ENU8Ac", delegate: delegate) { result in + switch result { + case .success(let rewards): + XCTAssert(rewards.previousReward == nil, rewards.previousReward?.amount.description ?? "") + + XCTAssert(rewards.estimatedPreviousReward == nil, rewards.estimatedPreviousReward?.amount.normalisedRepresentation ?? "") + + XCTAssert(rewards.estimatedNextReward?.amount.normalisedRepresentation == "0", rewards.estimatedNextReward?.amount.normalisedRepresentation ?? "") + XCTAssert(rewards.estimatedNextReward?.fee.description == "0.0499", rewards.estimatedNextReward?.fee.description ?? "") + XCTAssert(rewards.estimatedNextReward?.cycle.description == "745", rewards.estimatedNextReward?.cycle.description ?? "") + XCTAssert(rewards.estimatedNextReward?.bakerAlias == "Teztillery", rewards.estimatedNextReward?.bakerAlias ?? "") + + XCTAssert(rewards.moreThan1CycleBetweenPreiousAndNext() == false) + + case .failure(let error): + XCTFail("Error: \(error)") + } + + expectation.fulfill() + } + + wait(for: [expectation], timeout: 120) + } + func testBakers() { let expectation = XCTestExpectation(description: "tzkt-bakers") diff --git a/Tests/KukaiCoreSwiftTests/MockConstants.swift b/Tests/KukaiCoreSwiftTests/MockConstants.swift index a61a2234..cffef781 100644 --- a/Tests/KukaiCoreSwiftTests/MockConstants.swift +++ b/Tests/KukaiCoreSwiftTests/MockConstants.swift @@ -36,7 +36,7 @@ public struct MockConstants { // MARK: - Init private init() { - config = TezosNodeClientConfig(withDefaultsForNetworkType: .testnet) + config = TezosNodeClientConfig(withDefaultsForNetworkType: .ghostnet) loggingConfig = LoggingConfig(logNetworkFailures: true, logNetworkSuccesses: true) let sessionConfig = URLSessionConfiguration.ephemeral // Uses no caching / storage @@ -90,6 +90,12 @@ public struct MockConstants { var tzktDelegatorRewardsURL = tzktURL.appendingPathComponent("v1/rewards/delegators/tz1Ue76bLW7boAcJEZf2kSGcamdBKVi4Kpss") tzktDelegatorRewardsURL.appendQueryItem(name: "limit", value: 25) + var tzktDelegatorRewardsNoPreviousURL = tzktURL.appendingPathComponent("v1/rewards/delegators/tz1iv8r8UUCEZK5gqpLPnMPzP4VRJBJUdGgr") + tzktDelegatorRewardsNoPreviousURL.appendQueryItem(name: "limit", value: 25) + + var tzktDelegatorRewardsNoneURL = tzktURL.appendingPathComponent("v1/rewards/delegators/tz1ckwbvP7pdTLS1aAe6YPoiKpG2d8ENU8Ac") + tzktDelegatorRewardsNoneURL.appendQueryItem(name: "limit", value: 25) + var bakingBadConfigURL1 = bakingBadURL.appendingPathComponent("v2/bakers/tz1fwnfJNgiDACshK9avfRfFbMaXrs3ghoJa") bakingBadConfigURL1.appendQueryItem(name: "configs", value: "true") @@ -99,6 +105,12 @@ public struct MockConstants { var bakingBadConfigURL3 = bakingBadURL.appendingPathComponent("v2/bakers/tz1S5WxdZR5f9NzsPXhr7L9L1vrEb5spZFur") bakingBadConfigURL3.appendQueryItem(name: "configs", value: "true") + var bakingBadConfigURL4 = bakingBadURL.appendingPathComponent("v2/bakers/tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM") + bakingBadConfigURL4.appendQueryItem(name: "configs", value: "true") + + var bakingBadConfigURL5 = bakingBadURL.appendingPathComponent("v2/bakers/tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4") + bakingBadConfigURL5.appendQueryItem(name: "configs", value: "true") + var tzktsuggestURL1 = tzktURL.appendingPathComponent("v1/suggest/accounts/Bake Nug Payouts") tzktsuggestURL1.appendQueryItem(name: "limit", value: 1) @@ -111,6 +123,9 @@ public struct MockConstants { var tzktsuggestURL4 = tzktURL.appendingPathComponent("v1/suggest/accounts/Baking Benjamins Payouts") tzktsuggestURL4.appendQueryItem(name: "limit", value: 1) + var tzktsuggestURL5 = tzktURL.appendingPathComponent("v1/suggest/accounts/Teztillery Payouts") + tzktsuggestURL5.appendQueryItem(name: "limit", value: 1) + var tzktLastBakerRewardURL = tzktURL.appendingPathComponent("v1/accounts/tz1Ue76bLW7boAcJEZf2kSGcamdBKVi4Kpss/operations") tzktLastBakerRewardURL.appendQueryItem(name: "limit", value: 1) tzktLastBakerRewardURL.appendQueryItem(name: "type", value: "transaction") @@ -126,6 +141,16 @@ public struct MockConstants { tzktLastBakerRewardURL3.appendQueryItem(name: "type", value: "transaction") tzktLastBakerRewardURL3.appendQueryItem(name: "sender.in", value: "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv,tz1S5WxdZR5f9NzsPXhr7L9L1vrEb5spZFur,tz1gnuBF9TbBcgHPV2mUE96tBrW7PxqRmx1h") + var tzktLastBakerRewardURL4 = tzktURL.appendingPathComponent("v1/accounts/tz1iv8r8UUCEZK5gqpLPnMPzP4VRJBJUdGgr/operations") + tzktLastBakerRewardURL4.appendQueryItem(name: "limit", value: 1) + tzktLastBakerRewardURL4.appendQueryItem(name: "type", value: "transaction") + tzktLastBakerRewardURL4.appendQueryItem(name: "sender.in", value: "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv,tz1ShireJgwr8ag5dETMY4RNqkXeu1YgyDYC") + + var tzktLastBakerRewardURL5 = tzktURL.appendingPathComponent("v1/accounts/tz1ckwbvP7pdTLS1aAe6YPoiKpG2d8ENU8Ac/operations") + tzktLastBakerRewardURL5.appendQueryItem(name: "limit", value: 1) + tzktLastBakerRewardURL5.appendQueryItem(name: "type", value: "transaction") + tzktLastBakerRewardURL5.appendQueryItem(name: "sender.in", value: "tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4") + var tzktDelegatesURL = tzktURL.appendingPathComponent("v1/delegates") tzktDelegatesURL.appendQueryItem(name: "select.values", value: "address,alias,balance,stakingBalance") tzktDelegatesURL.appendQueryItem(name: "active", value: "true") @@ -179,16 +204,23 @@ public struct MockConstants { tzktBalancePageURL: (MockConstants.jsonStub(fromFilename: "tzkt_balance-page"), MockConstants.http200), tzktCyclesURL: (MockConstants.jsonStub(fromFilename: "tzkt_cycles"), MockConstants.http200), tzktDelegatorRewardsURL: (MockConstants.jsonStub(fromFilename: "tzkt_delegator-rewards"), MockConstants.http200), + tzktDelegatorRewardsNoPreviousURL: (MockConstants.jsonStub(fromFilename: "tzkt_delegator-rewards-no-previous"), MockConstants.http200), + tzktDelegatorRewardsNoneURL: (MockConstants.jsonStub(fromFilename: "tzkt_delegator-rewards-none"), MockConstants.http200), bakingBadConfigURL1: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1fwnfJNgiDACshK9avfRfFbMaXrs3ghoJa"), MockConstants.http200), bakingBadConfigURL2: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv"), MockConstants.http200), bakingBadConfigURL3: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1S5WxdZR5f9NzsPXhr7L9L1vrEb5spZFur"), MockConstants.http200), + bakingBadConfigURL4: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM"), MockConstants.http200), + bakingBadConfigURL5: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4"), MockConstants.http200), tzktsuggestURL1: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-bake-nug"), MockConstants.http200), tzktsuggestURL2: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-the-shire"), MockConstants.http200), tzktsuggestURL3: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-the-shire_updated"), MockConstants.http200), tzktsuggestURL4: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-baking-benjamins"), MockConstants.http200), + tzktsuggestURL5: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-teztillery"), MockConstants.http200), tzktLastBakerRewardURL: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment"), MockConstants.http200), tzktLastBakerRewardURL2: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment"), MockConstants.http200), tzktLastBakerRewardURL3: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment_updated"), MockConstants.http200), + tzktLastBakerRewardURL4: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment_updated"), MockConstants.http200), + tzktLastBakerRewardURL5: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment_updated"), MockConstants.http200), tzktDelegatesURL: (MockConstants.jsonStub(fromFilename: "tzkt_ghostnet-bakers"), MockConstants.http200), // Media proxy @@ -381,7 +413,7 @@ public struct MockConstants { public static let token10Decimals = Token(name: "Token 10 decimals", symbol: "TK10", tokenType: .fungible, faVersion: .fa2, balance: TokenAmount.zero(), thumbnailURL: nil, tokenContractAddress: "KT1G1cCRNBgQ48mVDjopHjEmTN5Sbtar8nn9", tokenId: 0, nfts: nil, mintingTool: nil) public static let nftMetadata = TzKTBalanceMetadata(name: "NFT Name", symbol: "NFT", decimals: "0", formats: nil, displayUri: MediaProxySerivceTests.ipfsURIWithoutExtension, artifactUri: nil, thumbnailUri: MediaProxySerivceTests.ipfsURIWithExtension, description: "A sample description", mintingTool: nil, tags: nil, minter: nil, shouldPreferSymbol: nil, attributes: nil, ttl: nil) public static let tokenWithNFTs = Token(name: "Token with NFTS", symbol: "T-NFT", tokenType: .nonfungible, faVersion: .fa2, balance: TokenAmount.zero(), thumbnailURL: nil, tokenContractAddress: "KT1G1cCRNBgQ48mVDjopHjEmTN5Sbtabc123", tokenId: 0, nfts: [ - NFT(fromTzKTBalance: TzKTBalance(balance: "1", token: TzKTBalanceToken(contract: TzKTAddress(alias: nil, address: "KT1G1cCRNBgQ48mVDjopHjEmTN5Sbtabc123"), tokenId: "4", standard: .fa2, totalSupply: "1", metadata: MockConstants.nftMetadata))) + NFT(fromTzKTBalance: TzKTBalance(balance: "1", token: TzKTBalanceToken(contract: TzKTAddress(alias: nil, address: "KT1G1cCRNBgQ48mVDjopHjEmTN5Sbtabc123"), tokenId: "4", standard: .fa2, totalSupply: "1", metadata: MockConstants.nftMetadata), firstLevel: 0, lastLevel: 0)) ], mintingTool: nil) diff --git a/Tests/KukaiCoreSwiftTests/Models/Config/TezosNodeClientConfigTests.swift b/Tests/KukaiCoreSwiftTests/Models/Config/TezosNodeClientConfigTests.swift index 63bc6a08..e775e236 100644 --- a/Tests/KukaiCoreSwiftTests/Models/Config/TezosNodeClientConfigTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/Config/TezosNodeClientConfigTests.swift @@ -20,7 +20,7 @@ class TezosNodeClientConfigTests: XCTestCase { } func testDefaults() { - let config1 = TezosNodeClientConfig(withDefaultsForNetworkType: .testnet) + let config1 = TezosNodeClientConfig(withDefaultsForNetworkType: .ghostnet) XCTAssert(config1.nodeURLs[0].absoluteString == "https://ghostnet.smartpy.io", config1.nodeURLs[0].absoluteString) XCTAssert(config1.nodeURLs[1].absoluteString == "https://rpc.ghostnet.tzboot.net", config1.nodeURLs[1].absoluteString) XCTAssert(config1.betterCallDevURL.absoluteString == "https://api.better-call.dev/", config1.betterCallDevURL.absoluteString) diff --git a/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift b/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift index c0d07ef7..28ddaaae 100644 --- a/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift @@ -21,8 +21,4 @@ class CurrentDeviceTests: XCTestCase { func testCurrentDevice() { XCTAssert(CurrentDevice.isSimulator == true) } - - func testBiometrics() { - XCTAssert(CurrentDevice.biometricTypeSupported() == .none) - } } diff --git a/Tests/KukaiCoreSwiftTests/Models/TokenTests.swift b/Tests/KukaiCoreSwiftTests/Models/TokenTests.swift index 4cd52dd0..3d03cf51 100644 --- a/Tests/KukaiCoreSwiftTests/Models/TokenTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/TokenTests.swift @@ -21,7 +21,7 @@ class TokenTests: XCTestCase { func testToken() { let token = Token(name: "test1", symbol: "T", tokenType: .fungible, faVersion: .fa1_2, balance: TokenAmount(fromNormalisedAmount: 3, decimalPlaces: 4), thumbnailURL: URL(string: "ipfs://abcdefgh1234"), tokenContractAddress: "KT1abc", tokenId: nil, nfts: nil, mintingTool: nil) - let tzktBalance = TzKTBalance(balance: "1", token: TzKTBalanceToken(contract: TzKTAddress(alias: "Test Alias", address: "KT1abc"), tokenId: "0", standard: .fa2, totalSupply: "1", metadata: nil)) + let tzktBalance = TzKTBalance(balance: "1", token: TzKTBalanceToken(contract: TzKTAddress(alias: "Test Alias", address: "KT1abc"), tokenId: "0", standard: .fa2, totalSupply: "1", metadata: nil), firstLevel: 0, lastLevel: 0) let nft = NFT(fromTzKTBalance: tzktBalance) let token2 = Token(name: "test2", symbol: "F", tokenType: .nonfungible, faVersion: .fa2, balance: TokenAmount.zero(), thumbnailURL: URL(string: "ipfs://abcdefgh1234"), tokenContractAddress: "KT1abc", tokenId: 0, nfts: [nft], mintingTool: nil) diff --git a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift index 0be2ccf4..2c2fd620 100644 --- a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift @@ -78,7 +78,7 @@ final class TzKTTransactionTests: XCTestCase { } func testPlaceholders() { - let source = WalletMetadata(address: "tz1abc", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + let source = WalletMetadata(address: "tz1abc", derivationPath: nil, hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) let placeholder1 = TzKTTransaction.placeholder(withStatus: .unconfirmed, id: 567, opHash: "abc123", type: .transaction, counter: 0, fromWallet: source, newDelegate: TzKTAddress(alias: "Baking Benjamins", address: "tz1YgDUQV2eXm8pUWNz3S5aWP86iFzNp4jnD")) let placeholder2 = TzKTTransaction.placeholder(withStatus: .unconfirmed, id: 456, opHash: "def456", type: .transaction, counter: 1, fromWallet: source, destination: TzKTAddress(alias: nil, address: "tz1def"), xtzAmount: .init(fromNormalisedAmount: 4.17, decimalPlaces: 6), parameters: nil, primaryToken: nil, baker: nil, kind: nil) @@ -97,7 +97,7 @@ final class TzKTTransactionTests: XCTestCase { transaction.processAdditionalData(withCurrentWalletAddress: "tz1abc") if let json = try? JSONEncoder().encode(transaction), let jsonString = String(data: json, encoding: .utf8) { - XCTAssert(jsonString.count == 944, jsonString.count.description) + XCTAssert(jsonString.count == 973, jsonString.count.description) } else { XCTFail() } diff --git a/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift index 83ccac29..6dacefe0 100644 --- a/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift @@ -18,11 +18,11 @@ class ErrorHandlingServiceTests: XCTestCase { func testStaticConstrutors() { let error1 = KukaiError.rpcError(rpcErrorString: "testing RPC string", andFailWith: nil, requestURL: nil) XCTAssert(error1.rpcErrorString == "testing RPC string", error1.rpcErrorString ?? "-") - XCTAssert(error1.description == "RPC: testing RPC string", error1.description) + XCTAssert(error1.description == "testing RPC string", error1.description) let error2 = KukaiError.rpcError(rpcErrorString: "testing RPC string", andFailWith: FailWith(string: nil, int: "1", args: nil), requestURL: nil) XCTAssert(error2.rpcErrorString == "testing RPC string", error2.rpcErrorString ?? "-") - XCTAssert(error2.description == "RPC: testing RPC string", error2.description) + XCTAssert(error2.description == "testing RPC string", error2.description) let error3 = KukaiError.unknown(withString: "test unknown string") XCTAssert(error3.rpcErrorString == "test unknown string", error3.rpcErrorString ?? "-") @@ -30,7 +30,7 @@ class ErrorHandlingServiceTests: XCTestCase { let error4 = KukaiError.internalApplicationError(error: URLError(URLError.unknown)) XCTAssert(error4.rpcErrorString == nil, error4.rpcErrorString ?? "-") - XCTAssert(error4.description == "Internal Application Error: The operation couldn’t be completed. (NSURLErrorDomain error -1.)", error4.description) + XCTAssert(error4.description == "Internal Application Error: Error Domain=NSURLErrorDomain Code=-1 \"(null)\"", error4.description) let error5 = KukaiError.systemError(subType: URLError(URLError.unknown)) XCTAssert(error5.rpcErrorString == nil, error5.rpcErrorString ?? "-") @@ -105,8 +105,8 @@ class ErrorHandlingServiceTests: XCTestCase { let containsErrors1 = ErrorHandlingService.searchOperationResponseForErrors(ops, requestURL: nil) XCTAssert(containsErrors1?.errorType == .rpc) - XCTAssert(containsErrors1?.rpcErrorString == "gas_exhausted.operation", containsErrors1?.rpcErrorString ?? "-") - XCTAssert(containsErrors1?.description == "RPC: gas_exhausted.operation", containsErrors1?.description ?? "-") + XCTAssert(containsErrors1?.rpcErrorString == "RPC error code: gas_exhausted.operation", containsErrors1?.rpcErrorString ?? "-") + XCTAssert(containsErrors1?.description == "RPC error code: gas_exhausted.operation", containsErrors1?.description ?? "-") } func testOperationResponseParserFailWith() { @@ -127,7 +127,7 @@ class ErrorHandlingServiceTests: XCTestCase { let containsErrors1 = ErrorHandlingService.searchOperationResponseForErrors(ops, requestURL: nil) XCTAssert(containsErrors1?.errorType == .rpc) XCTAssert(containsErrors1?.rpcErrorString == "A FAILWITH instruction was reached: {\"int\": 14}", containsErrors1?.rpcErrorString ?? "-") - XCTAssert(containsErrors1?.description == "RPC: A FAILWITH instruction was reached: {\"int\": 14}", containsErrors1?.description ?? "-") + XCTAssert(containsErrors1?.description == "A FAILWITH instruction was reached: {\"int\": 14}", containsErrors1?.description ?? "-") } func testJsonResponse() { @@ -139,8 +139,8 @@ class ErrorHandlingServiceTests: XCTestCase { } let result2 = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil) - XCTAssert(result2?.rpcErrorString == "gas_exhausted.operation", result2?.rpcErrorString ?? "-") - XCTAssert(result2?.description == "RPC: gas_exhausted.operation", result2?.description ?? "-") + XCTAssert(result2?.rpcErrorString == "RPC error code: gas_exhausted.operation", result2?.rpcErrorString ?? "-") + XCTAssert(result2?.description == "RPC error code: gas_exhausted.operation", result2?.description ?? "-") } @@ -154,7 +154,7 @@ class ErrorHandlingServiceTests: XCTestCase { let result = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil) XCTAssert(result?.rpcErrorString == "A FAILWITH instruction was reached: {\"string\": Dex/wrong-min-out}", result?.rpcErrorString ?? "-") - XCTAssert(result?.description == "RPC: A FAILWITH instruction was reached: {\"string\": Dex/wrong-min-out}", result?.description ?? "-") + XCTAssert(result?.description == "A FAILWITH instruction was reached: {\"string\": Dex/wrong-min-out}", result?.description ?? "-") } func testNotEnoughBalanceResponse() { @@ -166,8 +166,21 @@ class ErrorHandlingServiceTests: XCTestCase { } let result = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil) - XCTAssert(result?.rpcErrorString == "A FAILWITH instruction was reached: {\"args\": [[\"string\": \"NotEnoughBalance\"]]}", result?.rpcErrorString ?? "-") - XCTAssert(result?.description == "RPC: A FAILWITH instruction was reached: {\"args\": [[\"string\": \"NotEnoughBalance\"]]}", result?.description ?? "-") + XCTAssert(result?.rpcErrorString == "Insufficient token balance to complete the transaction", result?.rpcErrorString ?? "-") + XCTAssert(result?.description == "Insufficient token balance to complete the transaction", result?.description ?? "-") + } + + func testInsufficientBalance() { + let errorData = MockConstants.jsonStub(fromFilename: "rpc_error_fa2_insufficient_balance") + + guard let opResponse = try? JSONDecoder().decode(OperationResponse.self, from: errorData) else { + XCTFail("Couldn't parse data as [OperationResponse]") + return + } + + let result = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil) + XCTAssert(result?.rpcErrorString == "Insufficient NFT/DeFi token balance to complete the transaction", result?.rpcErrorString ?? "-") + XCTAssert(result?.description == "Insufficient NFT/DeFi token balance to complete the transaction", result?.description ?? "-") } func testFailWithParsers() { diff --git a/Tests/KukaiCoreSwiftTests/Services/NetworkServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/NetworkServiceTests.swift index dfee01cc..403f284d 100644 --- a/Tests/KukaiCoreSwiftTests/Services/NetworkServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/NetworkServiceTests.swift @@ -168,7 +168,7 @@ class NetworkServiceTests: XCTestCase { case .failure(let error): XCTAssert(error.errorType == .rpc) - XCTAssert(error.description == "RPC: contract.counter_in_the_future", error.description) + XCTAssert(error.description == "RPC error code: contract.counter_in_the_future", error.description) XCTAssert(error.requestURL?.absoluteString == MockConstants.shared.config.nodeURLs[1].appendingPathComponent("chains/main/blocks/head/helpers/preapply/operations").absoluteString, error.requestURL?.absoluteString ?? "-") } diff --git a/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift index 26f23b12..fe58b86f 100644 --- a/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift @@ -53,24 +53,4 @@ class OperationServiceTests: XCTestCase { wait(for: [expectation], timeout: 120) } - - func testLedgerWithoutReveal() { - let stringToSign = operationService.ledgerStringToSign( - forgedHash: "43f597d84037e88354ed041cc6356f737cc6638691979bb64415451b58b4af2c6c00ad00bb6cbcfc497bffbaf54c23511c74dbeafb2d00ffde080000c0843d00005134b25890279835eb946e6369a3d719bc0d617700", - operationPayload: MockConstants.sendOperationPayload - ) - - // Should be an operation starting with "03" - XCTAssert(stringToSign == "0343f597d84037e88354ed041cc6356f737cc6638691979bb64415451b58b4af2c6c00ad00bb6cbcfc497bffbaf54c23511c74dbeafb2d00ffde080000c0843d00005134b25890279835eb946e6369a3d719bc0d617700", stringToSign) - } - - func testLedgerWithReveal() { - let stringToSign = operationService.ledgerStringToSign( - forgedHash: "43f597d84037e88354ed041cc6356f737cc6638691979bb64415451b58b4af2c6c00ad00bb6cbcfc497bffbaf54c23511c74dbeafb2d00ffde080000c0843d00005134b25890279835eb946e6369a3d719bc0d617700", - operationPayload: MockConstants.sendOperationWithRevealPayload - ) - - // Should be a 32 character long blake2b hash - XCTAssert(stringToSign == "a49d693a7bf5c22564bbb9368c94362ea64f07e5c5d1c443b63190ae5c85adf2", stringToSign) - } } diff --git a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift index 7e19da89..3dcb55a9 100644 --- a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift @@ -211,7 +211,6 @@ class WalletCacheServiceTests: XCTestCase { XCTFail("Should not error: \(error)") } - // Update one and check let _ = list.set(hdWalletGroupName: "Blah 2", forAddress: hdWallet2.address) let _ = walletCacheService.encryptAndWriteMetadataToDisk(list) @@ -250,22 +249,22 @@ class WalletCacheServiceTests: XCTestCase { func testMetadata() { let mainentDomain = [TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.tez", address: "tz1abc123"))] let ghostnetDomain = [TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.gho", address: "tz1abc123"))] - let metadata1 = WalletMetadata(address: "tz1abc123", hdWalletGroupName: nil, mainnetDomains: mainentDomain, ghostnetDomains: ghostnetDomain, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + let metadata1 = WalletMetadata(address: "tz1abc123", derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: mainentDomain, ghostnetDomains: ghostnetDomain, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) XCTAssert(metadata1.hasMainnetDomain()) XCTAssert(metadata1.hasGhostnetDomain()) XCTAssert(metadata1.hasDomain(onNetwork: TezosNodeClientConfig.NetworkType.mainnet)) - XCTAssert(metadata1.hasDomain(onNetwork: TezosNodeClientConfig.NetworkType.testnet)) + XCTAssert(metadata1.hasDomain(onNetwork: TezosNodeClientConfig.NetworkType.ghostnet)) XCTAssert(metadata1.mainnetDomains?.first?.domain.name == "blah.tez") XCTAssert(metadata1.ghostnetDomains?.first?.domain.name == "blah.gho") XCTAssert(metadata1.primaryMainnetDomain()?.domain.name == "blah.tez") XCTAssert(metadata1.primaryGhostnetDomain()?.domain.name == "blah.gho") XCTAssert(metadata1.primaryDomain(onNetwork: .mainnet)?.domain.name == "blah.tez") - XCTAssert(metadata1.primaryDomain(onNetwork: .testnet)?.domain.name == "blah.gho") + XCTAssert(metadata1.primaryDomain(onNetwork: .ghostnet)?.domain.name == "blah.gho") - let metadata2 = WalletMetadata(address: "tz1def456", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + let metadata2 = WalletMetadata(address: "tz1def456", derivationPath: nil, hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) XCTAssert(!metadata2.hasMainnetDomain()) XCTAssert(!metadata2.hasGhostnetDomain()) @@ -274,23 +273,23 @@ class WalletCacheServiceTests: XCTestCase { func testMetadataList() { let mainentDomain = TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.tez", address: "tz1abc123")) let ghostnetDomain = TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.gho", address: "tz1abc123")) - let child = WalletMetadata(address: "tz1child", hdWalletGroupName: nil, type: .hd, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) - let updatedWatch = WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "blah", backedUp: true) + let child = WalletMetadata(address: "tz1child", derivationPath: nil, hdWalletGroupName: nil, type: .hd, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + let updatedWatch = WalletMetadata(address: "tz1jkl", derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "blah", backedUp: true) let hd: [WalletMetadata] = [ - WalletMetadata(address: "tz1abc123", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [child], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + WalletMetadata(address: "tz1abc123", derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [child], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) ] let social: [WalletMetadata] = [ - WalletMetadata(address: "tz1def", hdWalletGroupName: nil, socialUsername: "test@gmail.com", socialType: .google, type: .social, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + WalletMetadata(address: "tz1def", derivationPath: nil, hdWalletGroupName: nil, socialUsername: "test@gmail.com", socialType: .google, type: .social, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) ] let linear: [WalletMetadata] = [ - WalletMetadata(address: "tz1ghi", hdWalletGroupName: nil, type: .regular, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + WalletMetadata(address: "tz1ghi", derivationPath: nil, hdWalletGroupName: nil, type: .regular, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) ] let watch: [WalletMetadata] = [ - WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + WalletMetadata(address: "tz1jkl", derivationPath: nil, hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) ] - var list = WalletMetadataList(socialWallets: social, hdWallets: hd, linearWallets: linear, ledgerWallets: [], watchWallets: watch) + let list = WalletMetadataList(socialWallets: social, hdWallets: hd, linearWallets: linear, ledgerWallets: [], watchWallets: watch) let addresses = list.addresses() XCTAssert(addresses == ["tz1def", "tz1abc123", "tz1child", "tz1ghi", "tz1jkl"], addresses.description) @@ -334,15 +333,133 @@ class WalletCacheServiceTests: XCTestCase { func testWatchWallet() { XCTAssert(walletCacheService.deleteAllCacheAndKeys()) - let watchWallet = WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + let watchWallet = WalletMetadata(address: "tz1jkl", derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) do { try walletCacheService.cacheWatchWallet(metadata: watchWallet) - let list = walletCacheService.readMetadataFromDiskAndDecrypt() + let list = walletCacheService.readMetadataFromDiskAndDecrypt() let watch = list.watchWallets XCTAssert(watch.count == 1, watch.count.description) } catch { XCTFail("Should not error: \(error)") } } + + func testWatchWalletThatAlreadyExists() { + XCTAssert(walletCacheService.deleteAllCacheAndKeys()) + + do { + try walletCacheService.cache(wallet: MockConstants.defaultLinearWallet, childOfIndex: nil, backedUp: false) + } catch { + XCTFail("Should not error: \(error)") + } + + let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + + do { + try walletCacheService.cacheWatchWallet(metadata: watchWallet) + XCTFail("Already exists, should be rejected") + } catch let error { + XCTAssert(error.localizedDescription == "The operation couldn’t be completed. (KukaiCoreSwift.WalletCacheError error 8.)", error.localizedDescription) + } + } + + func testWatchWalletRemovedAfterImported() { + XCTAssert(walletCacheService.deleteAllCacheAndKeys()) + + // Add watch and confirm + let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + + do { + try walletCacheService.cacheWatchWallet(metadata: watchWallet) + let list = walletCacheService.readMetadataFromDiskAndDecrypt() + let watch = list.watchWallets + XCTAssert(watch.count == 1, watch.count.description) + } catch let error { + XCTFail("Should not error: \(error)") + } + + + // Add the wallet via an import, test watch is now been removed + do { + try walletCacheService.cache(wallet: MockConstants.defaultLinearWallet, childOfIndex: nil, backedUp: false) + let list = walletCacheService.readMetadataFromDiskAndDecrypt() + let watch = list.watchWallets + XCTAssert(watch.count == 0, watch.count.description) + } catch { + XCTFail("Should not error: \(error)") + } + } + + func testLedgerWalletCaching() { + XCTAssert(walletCacheService.deleteAllCacheAndKeys()) + + let ledgerWallet = LedgerWallet(address: "tz1abc", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah1")! + let ledgerWalletChild1 = LedgerWallet(address: "tz1def", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah2")! + let ledgerWalletChild2 = LedgerWallet(address: "tz1ghi", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah3")! + let ledgerWalletChild3 = LedgerWallet(address: "tz1jkl", publicKey: "edpks1234", derivationPath: "m/44'/1729'/147'/62'", curve: .ed25519, ledgerUUID: "blah4")! + + do { + try walletCacheService.cache(wallet: ledgerWallet, childOfIndex: nil, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild1, childOfIndex: 0, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild2, childOfIndex: 0, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild3, childOfIndex: 0, backedUp: true) + + let list1 = walletCacheService.readMetadataFromDiskAndDecrypt() + let ledgers1 = list1.ledgerWallets + XCTAssert(ledgers1.count == 1, ledgers1.count.description) + XCTAssert(ledgers1[0].children.count == 3, ledgers1[0].children.count.description) + XCTAssert(ledgers1[0].children.last?.derivationPath == "m/44'/1729'/147'/62'", ledgers1[0].children.last?.derivationPath ?? "-") + + + let _ = WalletCacheService().deleteWallet(withAddress: "tz1abc", parentIndex: nil) + let list2 = walletCacheService.readMetadataFromDiskAndDecrypt() + let ledgers2 = list2.ledgerWallets + XCTAssert(ledgers2.count == 0, ledgers2.count.description) + + let testWalletsGone1 = WalletCacheService().fetchWallet(forAddress: "tz1abc") + XCTAssert(testWalletsGone1 == nil) + + let testWalletsGone2 = WalletCacheService().fetchWallet(forAddress: "tz1def") + XCTAssert(testWalletsGone2 == nil) + + } catch let error { + XCTFail("Should not error: \(error)") + } + } + + func testLedgerWalletMigrate() { + XCTAssert(walletCacheService.deleteAllCacheAndKeys()) + + let ledgerWallet = LedgerWallet(address: "tz1abc", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah1")! + let ledgerWalletChild1 = LedgerWallet(address: "tz1def", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah1")! + let ledgerWalletChild2 = LedgerWallet(address: "tz1ghi", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah1")! + + do { + try walletCacheService.cache(wallet: ledgerWallet, childOfIndex: nil, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild1, childOfIndex: 0, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild2, childOfIndex: 0, backedUp: true) + + let list = walletCacheService.readMetadataFromDiskAndDecrypt() + guard let ledger = list.ledgerWallets.first else { + XCTFail("Ledger list empty") + return + } + + XCTAssert(walletCacheService.migrateLedger(metadata: ledger, toNewUUID: "migrated")) + + let ledgerParent = walletCacheService.fetchWallet(forAddress: ledgerWallet.address) as? LedgerWallet + XCTAssert(ledgerParent?.ledgerUUID == "migrated") + + let ledgerChild1 = walletCacheService.fetchWallet(forAddress: ledgerWalletChild1.address) as? LedgerWallet + XCTAssert(ledgerChild1?.ledgerUUID == "migrated") + + let ledgerChild2 = walletCacheService.fetchWallet(forAddress: ledgerWalletChild2.address) as? LedgerWallet + XCTAssert(ledgerChild2?.ledgerUUID == "migrated") + + + } catch let error { + XCTFail("Should not error: \(error)") + } + } } diff --git a/Tests/KukaiCoreSwiftTests/Stubs/rpc_error_fa2_insufficient_balance.json b/Tests/KukaiCoreSwiftTests/Stubs/rpc_error_fa2_insufficient_balance.json new file mode 100644 index 00000000..ee823c71 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/rpc_error_fa2_insufficient_balance.json @@ -0,0 +1,330 @@ +{ + "contents": [ + { + "kind": "transaction", + "source": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "fee": "0", + "counter": "6856031", + "gas_limit": "1040000", + "storage_limit": "60000", + "amount": "0", + "destination": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "parameters": { + "entrypoint": "approve", + "value": { + "prim": "Pair", + "args": [ + { + "string": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc" + }, + { + "int": "0" + } + ] + } + }, + "metadata": { + "operation_result": { + "status": "backtracked", + "consumed_milligas": "3471406", + "storage_size": "215587", + "lazy_storage_diff": [ + { + "kind": "big_map", + "id": "31", + "diff": { + "action": "update", + "updates": [ + { + "key_hash": "exprtms8xryAx1EVZU2kyeTJhP8o6RBLBip81R7h7jS13dXJmREbhb", + "key": { + "bytes": "05070701000000066c65646765720a00000016000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + "value": { + "bytes": "050707000a0200000000" + } + } + ] + } + } + ] + } + } + }, + { + "kind": "transaction", + "source": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "fee": "0", + "counter": "6856032", + "gas_limit": "1040000", + "storage_limit": "60000", + "amount": "0", + "destination": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "parameters": { + "entrypoint": "approve", + "value": { + "prim": "Pair", + "args": [ + { + "string": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc" + }, + { + "int": "7349" + } + ] + } + }, + "metadata": { + "operation_result": { + "status": "backtracked", + "consumed_milligas": "3480969", + "storage_size": "215619", + "lazy_storage_diff": [ + { + "kind": "big_map", + "id": "31", + "diff": { + "action": "update", + "updates": [ + { + "key_hash": "exprtms8xryAx1EVZU2kyeTJhP8o6RBLBip81R7h7jS13dXJmREbhb", + "key": { + "bytes": "05070701000000066c65646765720a00000016000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + "value": { + "bytes": "050707000a020000002007040a0000001601ece491d1ef313570a669e1c6d96af52d1ce0785c0000b572" + } + } + ] + } + } + ] + } + } + }, + { + "kind": "transaction", + "source": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "fee": "0", + "counter": "6856033", + "gas_limit": "1040000", + "storage_limit": "60000", + "amount": "1000000", + "destination": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc", + "parameters": { + "entrypoint": "investLiquidity", + "value": { + "int": "7349" + } + }, + "metadata": { + "operation_result": { + "status": "backtracked", + "balance_updates": [ + { + "kind": "contract", + "contract": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "change": "-1000000", + "origin": "block" + }, + { + "kind": "contract", + "contract": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc", + "change": "1000000", + "origin": "block" + } + ], + "consumed_milligas": "8318704", + "storage_size": "83635", + "lazy_storage_diff": [ + { + "kind": "big_map", + "id": "1498", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1497", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1496", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1495", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1494", + "diff": { + "action": "update", + "updates": [ + { + "key_hash": "exprvNa7fxN79ehSzqQiNv6YMNBFbPnt9wUWhvKJpD2KdcmicK4R3f", + "key": { + "bytes": "000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + "value": { + "prim": "Pair", + "args": [ + { + "int": "289061751656371524" + }, + { + "int": "100397669321122290545" + } + ] + } + } + ] + } + }, + { + "kind": "big_map", + "id": "1493", + "diff": { + "action": "update", + "updates": [ + { + "key_hash": "exprvNa7fxN79ehSzqQiNv6YMNBFbPnt9wUWhvKJpD2KdcmicK4R3f", + "key": { + "bytes": "000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + "value": { + "prim": "Pair", + "args": [ + { + "prim": "Pair", + "args": [ + [], + { + "int": "1487105" + } + ] + }, + { + "int": "0" + } + ] + } + } + ] + } + }, + { + "kind": "big_map", + "id": "1492", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1491", + "diff": { + "action": "update", + "updates": [] + } + } + ] + }, + "internal_operation_results": [ + { + "kind": "transaction", + "source": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc", + "nonce": 0, + "amount": "0", + "destination": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "parameters": { + "entrypoint": "transfer", + "value": { + "prim": "Pair", + "args": [ + { + "bytes": "000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + { + "prim": "Pair", + "args": [ + { + "bytes": "01ece491d1ef313570a669e1c6d96af52d1ce0785c00" + }, + { + "int": "7348" + } + ] + } + ] + } + }, + "result": { + "status": "failed", + "errors": [ + { + "kind": "temporary", + "id": "proto.013-PtJakart.michelson_v1.runtime_error", + "contract_handle": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "contract_code": "Deprecated" + }, + { + "kind": "temporary", + "id": "proto.013-PtJakart.michelson_v1.script_rejected", + "location": 519, + "with": { + "string": "fa2_insufficient_balance" + } + } + ] + } + } + ] + } + }, + { + "kind": "transaction", + "source": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "fee": "0", + "counter": "6856034", + "gas_limit": "1040000", + "storage_limit": "60000", + "amount": "0", + "destination": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "parameters": { + "entrypoint": "approve", + "value": { + "prim": "Pair", + "args": [ + { + "string": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc" + }, + { + "int": "0" + } + ] + } + }, + "metadata": { + "operation_result": { + "status": "skipped" + } + } + } + ] +} diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM.json new file mode 100644 index 00000000..3592d89d --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM.json @@ -0,0 +1,139 @@ +{ + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv", + "name": "The Shire", + "logo": "https://services.tzkt.io/v1/avatars/tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv", + "balance": 5326177.599918, + "stakingBalance": 44473420.298689, + "stakingCapacity": 53261775.99918, + "maxStakingBalance": 53261775.99918, + "freeSpace": 8788355.700490996, + "fee": 0.15, + "minDelegation": 0, + "payoutDelay": 1, + "payoutPeriod": 1, + "openForDelegation": true, + "estimatedRoi": 0.053113, + "serviceType": "multiasset", + "serviceHealth": "active", + "payoutTiming": "stable", + "payoutAccuracy": "precise", + "audit": "601aabbe5e0a85c68fb18d13", + "insuranceCoverage": 0, + "config": { + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv", + "fee": [ + { + "cycle": 559, + "value": 0.15 + }, + { + "cycle": 473, + "value": 0.109 + }, + { + "cycle": 468, + "value": 0.05 + }, + { + "cycle": 440, + "value": 0.109 + }, + { + "cycle": 437, + "value": 0.12 + }, + { + "cycle": 407, + "value": 0.15 + }, + { + "cycle": 265, + "value": 0.08 + }, + { + "cycle": 0, + "value": 0.05 + } + ], + "minDelegation": [ + { + "cycle": 0, + "value": 0 + } + ], + "payoutFee": [ + { + "cycle": 0, + "value": false + } + ], + "payoutDelay": [ + { + "cycle": 467, + "value": 1 + }, + { + "cycle": 466, + "value": 2 + }, + { + "cycle": 465, + "value": 3 + }, + { + "cycle": 464, + "value": 4 + }, + { + "cycle": 463, + "value": 5 + }, + { + "cycle": 0, + "value": 6 + } + ], + "payoutPeriod": [ + { + "cycle": 0, + "value": 1 + } + ], + "minPayout": [ + { + "cycle": 0, + "value": 0.001 + } + ], + "rewardStruct": [ + { + "cycle": 0, + "value": 981 + } + ], + "payoutRatio": [ + { + "cycle": 0, + "value": 666 + } + ], + "maxStakingThreshold": [ + { + "cycle": 0, + "value": 1 + } + ], + "openForDelegation": [ + { + "cycle": 0, + "value": true + } + ], + "allocationFee": [ + { + "cycle": 0, + "value": false + } + ] + } + } \ No newline at end of file diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4.json new file mode 100644 index 00000000..817613a1 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4.json @@ -0,0 +1,21 @@ +{ + "address": "tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4", + "name": "Teztillery", + "logo": "https://services.tzkt.io/v1/avatars/tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4", + "balance": 2187.553015, + "stakingBalance": 10120.963766, + "stakingCapacity": 11897.932014, + "maxStakingBalance": 11897.932014, + "freeSpace": 3098.9606940000003, + "fee": 0.0499, + "minDelegation": 10, + "payoutDelay": 1, + "payoutPeriod": 1, + "openForDelegation": true, + "estimatedRoi": 0.05916, + "serviceType": "tezos_only", + "serviceHealth": "active", + "payoutTiming": "no_data", + "payoutAccuracy": "no_data", + "insuranceCoverage": 0 + } \ No newline at end of file diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-no-previous.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-no-previous.json new file mode 100644 index 00000000..f9027115 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-no-previous.json @@ -0,0 +1,314 @@ +[ + { + "cycle": 750, + "delegatedBalance": 452228, + "stakedBalance": 0, + "baker": { + "alias": "The Shire", + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv" + }, + "bakingPower": 44411942965285, + "totalBakingPower": 670376512904615, + "bakerDelegatedBalance": 973715068775, + "externalDelegatedBalance": 39035845408189, + "bakerStakedBalance": 4445506719663, + "externalStakedBalance": 0, + "expectedBlocks": 1628, + "expectedEndorsements": 11396991, + "futureBlocks": 1601, + "futureBlockRewards": 10670436057, + "blocks": 0, + "blockRewardsDelegated": 0, + "blockRewardsStakedOwn": 0, + "blockRewardsStakedEdge": 0, + "blockRewardsStakedShared": 0, + "missedBlocks": 0, + "missedBlockRewards": 0, + "futureEndorsements": 11394754, + "futureEndorsementRewards": 10849935432, + "endorsements": 0, + "endorsementRewardsDelegated": 0, + "endorsementRewardsStakedOwn": 0, + "endorsementRewardsStakedEdge": 0, + "endorsementRewardsStakedShared": 0, + "missedEndorsements": 0, + "missedEndorsementRewards": 0, + "blockFees": 0, + "missedBlockFees": 0, + "doubleBakingRewards": 0, + "doubleBakingLostStaked": 0, + "doubleBakingLostUnstaked": 0, + "doubleBakingLostExternalStaked": 0, + "doubleBakingLostExternalUnstaked": 0, + "doubleEndorsingRewards": 0, + "doubleEndorsingLostStaked": 0, + "doubleEndorsingLostUnstaked": 0, + "doubleEndorsingLostExternalStaked": 0, + "doubleEndorsingLostExternalUnstaked": 0, + "doublePreendorsingRewards": 0, + "doublePreendorsingLostStaked": 0, + "doublePreendorsingLostUnstaked": 0, + "doublePreendorsingLostExternalStaked": 0, + "doublePreendorsingLostExternalUnstaked": 0, + "vdfRevelationRewardsDelegated": 0, + "vdfRevelationRewardsStakedOwn": 0, + "vdfRevelationRewardsStakedEdge": 0, + "vdfRevelationRewardsStakedShared": 0, + "nonceRevelationRewardsDelegated": 0, + "nonceRevelationRewardsStakedOwn": 0, + "nonceRevelationRewardsStakedEdge": 0, + "nonceRevelationRewardsStakedShared": 0, + "nonceRevelationLosses": 0, + "blockRewardsLiquid": 0, + "endorsementRewardsLiquid": 0, + "nonceRevelationRewardsLiquid": 0, + "vdfRevelationRewardsLiquid": 0, + "revelationRewards": 0, + "revelationLosses": 0, + "doublePreendorsingLosses": 0, + "doubleEndorsingLosses": 0, + "doubleBakingLosses": 0, + "endorsementRewards": 0, + "blockRewards": 0, + "stakingBalance": 44455067196627, + "activeStake": 44411942965285, + "selectedStake": 670376512904615, + "balance": 452228, + "ownBlocks": 0, + "extraBlocks": 0, + "missedOwnBlocks": 0, + "missedExtraBlocks": 0, + "uncoveredOwnBlocks": 0, + "uncoveredExtraBlocks": 0, + "uncoveredEndorsements": 0, + "ownBlockRewards": 0, + "extraBlockRewards": 0, + "missedOwnBlockRewards": 0, + "missedExtraBlockRewards": 0, + "uncoveredOwnBlockRewards": 0, + "uncoveredExtraBlockRewards": 0, + "uncoveredEndorsementRewards": 0, + "ownBlockFees": 0, + "extraBlockFees": 0, + "missedOwnBlockFees": 0, + "missedExtraBlockFees": 0, + "uncoveredOwnBlockFees": 0, + "uncoveredExtraBlockFees": 0, + "doubleBakingLostDeposits": 0, + "doubleBakingLostRewards": 0, + "doubleBakingLostFees": 0, + "doubleEndorsingLostDeposits": 0, + "doubleEndorsingLostRewards": 0, + "doubleEndorsingLostFees": 0, + "revelationLostRewards": 0, + "revelationLostFees": 0 + }, + { + "cycle": 749, + "delegatedBalance": 629661, + "stakedBalance": 0, + "baker": { + "alias": "The Shire", + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv" + }, + "bakingPower": 44471812616608, + "totalBakingPower": 672634125017457, + "bakerDelegatedBalance": 946051522616, + "externalDelegatedBalance": 39089076546183, + "bakerStakedBalance": 4448347563200, + "externalStakedBalance": 0, + "expectedBlocks": 1624, + "expectedEndorsements": 11374051, + "futureBlocks": 1656, + "futureBlockRewards": 11037003192, + "blocks": 0, + "blockRewardsDelegated": 0, + "blockRewardsStakedOwn": 0, + "blockRewardsStakedEdge": 0, + "blockRewardsStakedShared": 0, + "missedBlocks": 0, + "missedBlockRewards": 0, + "futureEndorsements": 11375422, + "futureEndorsementRewards": 10828096552, + "endorsements": 0, + "endorsementRewardsDelegated": 0, + "endorsementRewardsStakedOwn": 0, + "endorsementRewardsStakedEdge": 0, + "endorsementRewardsStakedShared": 0, + "missedEndorsements": 0, + "missedEndorsementRewards": 0, + "blockFees": 0, + "missedBlockFees": 0, + "doubleBakingRewards": 0, + "doubleBakingLostStaked": 0, + "doubleBakingLostUnstaked": 0, + "doubleBakingLostExternalStaked": 0, + "doubleBakingLostExternalUnstaked": 0, + "doubleEndorsingRewards": 0, + "doubleEndorsingLostStaked": 0, + "doubleEndorsingLostUnstaked": 0, + "doubleEndorsingLostExternalStaked": 0, + "doubleEndorsingLostExternalUnstaked": 0, + "doublePreendorsingRewards": 0, + "doublePreendorsingLostStaked": 0, + "doublePreendorsingLostUnstaked": 0, + "doublePreendorsingLostExternalStaked": 0, + "doublePreendorsingLostExternalUnstaked": 0, + "vdfRevelationRewardsDelegated": 0, + "vdfRevelationRewardsStakedOwn": 0, + "vdfRevelationRewardsStakedEdge": 0, + "vdfRevelationRewardsStakedShared": 0, + "nonceRevelationRewardsDelegated": 0, + "nonceRevelationRewardsStakedOwn": 0, + "nonceRevelationRewardsStakedEdge": 0, + "nonceRevelationRewardsStakedShared": 0, + "nonceRevelationLosses": 0, + "blockRewardsLiquid": 0, + "endorsementRewardsLiquid": 0, + "nonceRevelationRewardsLiquid": 0, + "vdfRevelationRewardsLiquid": 0, + "revelationRewards": 0, + "revelationLosses": 0, + "doublePreendorsingLosses": 0, + "doubleEndorsingLosses": 0, + "doubleBakingLosses": 0, + "endorsementRewards": 0, + "blockRewards": 0, + "stakingBalance": 44483475631999, + "activeStake": 44471812616608, + "selectedStake": 672634125017457, + "balance": 629661, + "ownBlocks": 0, + "extraBlocks": 0, + "missedOwnBlocks": 0, + "missedExtraBlocks": 0, + "uncoveredOwnBlocks": 0, + "uncoveredExtraBlocks": 0, + "uncoveredEndorsements": 0, + "ownBlockRewards": 0, + "extraBlockRewards": 0, + "missedOwnBlockRewards": 0, + "missedExtraBlockRewards": 0, + "uncoveredOwnBlockRewards": 0, + "uncoveredExtraBlockRewards": 0, + "uncoveredEndorsementRewards": 0, + "ownBlockFees": 0, + "extraBlockFees": 0, + "missedOwnBlockFees": 0, + "missedExtraBlockFees": 0, + "uncoveredOwnBlockFees": 0, + "uncoveredExtraBlockFees": 0, + "doubleBakingLostDeposits": 0, + "doubleBakingLostRewards": 0, + "doubleBakingLostFees": 0, + "doubleEndorsingLostDeposits": 0, + "doubleEndorsingLostRewards": 0, + "doubleEndorsingLostFees": 0, + "revelationLostRewards": 0, + "revelationLostFees": 0 + }, + { + "cycle": 748, + "delegatedBalance": 799354, + "stakedBalance": 0, + "baker": { + "alias": "The Shire", + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv" + }, + "bakingPower": 44444439841760, + "totalBakingPower": 672533812378748, + "bakerDelegatedBalance": 922361570575, + "externalDelegatedBalance": 39110426817844, + "bakerStakedBalance": 4448087598714, + "externalStakedBalance": 0, + "expectedBlocks": 1624, + "expectedEndorsements": 11368745, + "futureBlocks": 954, + "futureBlockRewards": 6358273578, + "blocks": 659, + "blockRewardsDelegated": 3849280033, + "blockRewardsStakedOwn": 428088086, + "blockRewardsStakedEdge": 0, + "blockRewardsStakedShared": 0, + "missedBlocks": 2, + "missedBlockRewards": 12321546, + "futureEndorsements": 6992818, + "futureEndorsementRewards": 10823045240, + "endorsements": 4366738, + "endorsementRewardsDelegated": 0, + "endorsementRewardsStakedOwn": 0, + "endorsementRewardsStakedEdge": 0, + "endorsementRewardsStakedShared": 0, + "missedEndorsements": 8359, + "missedEndorsementRewards": 0, + "blockFees": 8503447, + "missedBlockFees": 55543, + "doubleBakingRewards": 0, + "doubleBakingLostStaked": 0, + "doubleBakingLostUnstaked": 0, + "doubleBakingLostExternalStaked": 0, + "doubleBakingLostExternalUnstaked": 0, + "doubleEndorsingRewards": 0, + "doubleEndorsingLostStaked": 0, + "doubleEndorsingLostUnstaked": 0, + "doubleEndorsingLostExternalStaked": 0, + "doubleEndorsingLostExternalUnstaked": 0, + "doublePreendorsingRewards": 0, + "doublePreendorsingLostStaked": 0, + "doublePreendorsingLostUnstaked": 0, + "doublePreendorsingLostExternalStaked": 0, + "doublePreendorsingLostExternalUnstaked": 0, + "vdfRevelationRewardsDelegated": 0, + "vdfRevelationRewardsStakedOwn": 0, + "vdfRevelationRewardsStakedEdge": 0, + "vdfRevelationRewardsStakedShared": 0, + "nonceRevelationRewardsDelegated": 0, + "nonceRevelationRewardsStakedOwn": 0, + "nonceRevelationRewardsStakedEdge": 0, + "nonceRevelationRewardsStakedShared": 0, + "nonceRevelationLosses": 0, + "blockRewardsLiquid": 3849280033, + "endorsementRewardsLiquid": 0, + "nonceRevelationRewardsLiquid": 0, + "vdfRevelationRewardsLiquid": 0, + "revelationRewards": 0, + "revelationLosses": 0, + "doublePreendorsingLosses": 0, + "doubleEndorsingLosses": 0, + "doubleBakingLosses": 0, + "endorsementRewards": 0, + "blockRewards": 4277368119, + "stakingBalance": 44480875987133, + "activeStake": 44444439841760, + "selectedStake": 672533812378748, + "balance": 799354, + "ownBlocks": 659, + "extraBlocks": 0, + "missedOwnBlocks": 2, + "missedExtraBlocks": 0, + "uncoveredOwnBlocks": 0, + "uncoveredExtraBlocks": 0, + "uncoveredEndorsements": 0, + "ownBlockRewards": 4277368119, + "extraBlockRewards": 0, + "missedOwnBlockRewards": 12321546, + "missedExtraBlockRewards": 0, + "uncoveredOwnBlockRewards": 0, + "uncoveredExtraBlockRewards": 0, + "uncoveredEndorsementRewards": 0, + "ownBlockFees": 8503447, + "extraBlockFees": 0, + "missedOwnBlockFees": 55543, + "missedExtraBlockFees": 0, + "uncoveredOwnBlockFees": 0, + "uncoveredExtraBlockFees": 0, + "doubleBakingLostDeposits": 0, + "doubleBakingLostRewards": 0, + "doubleBakingLostFees": 0, + "doubleEndorsingLostDeposits": 0, + "doubleEndorsingLostRewards": 0, + "doubleEndorsingLostFees": 0, + "revelationLostRewards": 0, + "revelationLostFees": 0 + } +] \ No newline at end of file diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-none.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-none.json new file mode 100644 index 00000000..24e24557 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-none.json @@ -0,0 +1,106 @@ +[ + { + "cycle": 752, + "delegatedBalance": 730253, + "stakedBalance": 0, + "baker": { + "alias": "Teztillery", + "address": "tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4" + }, + "bakingPower": 0, + "totalBakingPower": 378988343190364, + "bakerDelegatedBalance": 335560569, + "externalDelegatedBalance": 7535104289, + "bakerStakedBalance": 1321992446, + "externalStakedBalance": 0, + "expectedBlocks": 0, + "expectedEndorsements": 0, + "futureBlocks": 0, + "futureBlockRewards": 0, + "blocks": 0, + "blockRewardsDelegated": 0, + "blockRewardsStakedOwn": 0, + "blockRewardsStakedEdge": 0, + "blockRewardsStakedShared": 0, + "missedBlocks": 0, + "missedBlockRewards": 0, + "futureEndorsements": 0, + "futureEndorsementRewards": 0, + "endorsements": 0, + "endorsementRewardsDelegated": 0, + "endorsementRewardsStakedOwn": 0, + "endorsementRewardsStakedEdge": 0, + "endorsementRewardsStakedShared": 0, + "missedEndorsements": 0, + "missedEndorsementRewards": 0, + "blockFees": 0, + "missedBlockFees": 0, + "doubleBakingRewards": 0, + "doubleBakingLostStaked": 0, + "doubleBakingLostUnstaked": 0, + "doubleBakingLostExternalStaked": 0, + "doubleBakingLostExternalUnstaked": 0, + "doubleEndorsingRewards": 0, + "doubleEndorsingLostStaked": 0, + "doubleEndorsingLostUnstaked": 0, + "doubleEndorsingLostExternalStaked": 0, + "doubleEndorsingLostExternalUnstaked": 0, + "doublePreendorsingRewards": 0, + "doublePreendorsingLostStaked": 0, + "doublePreendorsingLostUnstaked": 0, + "doublePreendorsingLostExternalStaked": 0, + "doublePreendorsingLostExternalUnstaked": 0, + "vdfRevelationRewardsDelegated": 0, + "vdfRevelationRewardsStakedOwn": 0, + "vdfRevelationRewardsStakedEdge": 0, + "vdfRevelationRewardsStakedShared": 0, + "nonceRevelationRewardsDelegated": 0, + "nonceRevelationRewardsStakedOwn": 0, + "nonceRevelationRewardsStakedEdge": 0, + "nonceRevelationRewardsStakedShared": 0, + "nonceRevelationLosses": 0, + "blockRewardsLiquid": 0, + "endorsementRewardsLiquid": 0, + "nonceRevelationRewardsLiquid": 0, + "vdfRevelationRewardsLiquid": 0, + "revelationRewards": 0, + "revelationLosses": 0, + "doublePreendorsingLosses": 0, + "doubleEndorsingLosses": 0, + "doubleBakingLosses": 0, + "endorsementRewards": 0, + "blockRewards": 0, + "stakingBalance": 9192657304, + "activeStake": 0, + "selectedStake": 378988343190364, + "balance": 730253, + "ownBlocks": 0, + "extraBlocks": 0, + "missedOwnBlocks": 0, + "missedExtraBlocks": 0, + "uncoveredOwnBlocks": 0, + "uncoveredExtraBlocks": 0, + "uncoveredEndorsements": 0, + "ownBlockRewards": 0, + "extraBlockRewards": 0, + "missedOwnBlockRewards": 0, + "missedExtraBlockRewards": 0, + "uncoveredOwnBlockRewards": 0, + "uncoveredExtraBlockRewards": 0, + "uncoveredEndorsementRewards": 0, + "ownBlockFees": 0, + "extraBlockFees": 0, + "missedOwnBlockFees": 0, + "missedExtraBlockFees": 0, + "uncoveredOwnBlockFees": 0, + "uncoveredExtraBlockFees": 0, + "doubleBakingLostDeposits": 0, + "doubleBakingLostRewards": 0, + "doubleBakingLostFees": 0, + "doubleEndorsingLostDeposits": 0, + "doubleEndorsingLostRewards": 0, + "doubleEndorsingLostFees": 0, + "revelationLostRewards": 0, + "revelationLostFees": 0 + } + ] \ No newline at end of file diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_suggest-teztillery.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_suggest-teztillery.json new file mode 100644 index 00000000..d0fce608 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_suggest-teztillery.json @@ -0,0 +1,6 @@ +[ + { + "alias": "Tezex Bakery Payouts", + "address": "tz1cBvEkYWV7RjTkd2kEcMPLwP62pqivXNEi" + } + ] \ No newline at end of file