Skip to content

Commit

Permalink
Merge branch 'feature/verify_discover_and_explore' into develop
Browse files Browse the repository at this point in the history
# Conflicts:
#	Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • Loading branch information
simonmcl committed Jan 12, 2024
2 parents c531432 + 46b0fc0 commit 9cf7c05
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 54 deletions.
4 changes: 2 additions & 2 deletions Kukai Mobile/Services/DependencyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class DependencyManager {
activityService = ActivityService()
coinGeckoService = CoinGeckoService(networkService: tezosNodeClient.networkService)
tezosDomainsClient = TezosDomainsClient(networkService: tezosNodeClient.networkService, config: tezosClientConfig)
exploreService = ExploreService(networkService: tezosNodeClient.networkService)
exploreService = ExploreService(networkService: tezosNodeClient.networkService, networkType: .mainnet)
discoverService = DiscoverService(networkService: tezosNodeClient.networkService)
appUpdateService = AppUpdateService(networkService: tezosNodeClient.networkService)

Expand Down Expand Up @@ -246,7 +246,7 @@ class DependencyManager {
tzktClient = TzKTClient(networkService: tezosNodeClient.networkService, config: tezosClientConfig, betterCallDevClient: betterCallDevClient, dipDupClient: dipDupClient)
coinGeckoService = CoinGeckoService(networkService: tezosNodeClient.networkService)
tezosDomainsClient = TezosDomainsClient(networkService: tezosNodeClient.networkService, config: tezosClientConfig)
exploreService = ExploreService(networkService: tezosNodeClient.networkService)
exploreService = ExploreService(networkService: tezosNodeClient.networkService, networkType: currentNetworkType)
discoverService = DiscoverService(networkService: tezosNodeClient.networkService)
appUpdateService = AppUpdateService(networkService: tezosNodeClient.networkService)

Expand Down
6 changes: 4 additions & 2 deletions Kukai Mobile/Services/DiscoverService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import Foundation
import KukaiCoreSwift
import KukaiCryptoSwift
import OSLog

public struct DiscoverGroup: Codable, Hashable, Identifiable {
@DefaultUUID public var id: UUID
Expand All @@ -28,7 +30,7 @@ public struct DiscoverItem: Codable, Hashable, Identifiable {

public class DiscoverService {

private let discoverURL = "https://services.kukaiwallet.workers.dev/v1/discover"
private let discoverURL = "https://services.kukai.app/v2/discover?encode=true"

private let discoverCacheKey = "discover-cache-key"

Expand All @@ -54,7 +56,7 @@ public class DiscoverService {
}

// Request from API, no more frequently than once per day, else read cache
self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.fifteenMinute.rawValue, forKey: discoverCacheKey, responseType: [DiscoverGroup].self) { [weak self] result in
self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.fifteenMinute.rawValue, forKey: discoverCacheKey, responseType: [DiscoverGroup].self, isSecure: true) { [weak self] result in
guard let response = try? result.get() else {
completion(Result.failure(result.getFailure()))
return
Expand Down
110 changes: 69 additions & 41 deletions Kukai Mobile/Services/ExploreService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,20 @@
import Foundation
import KukaiCoreSwift

public enum ShouldDisplayLink: String, Codable {
case all
case none

public struct ExploreResponse: Codable {
let environment: ExploreEnvironments
}

public struct RemoteDiscoverItem: Codable {
let dappUrl: URL?
let category: [String]?
let discoverImageUrl: URL?
let hasZoomDiscoverImage: Bool?

enum CodingKeys: String, CodingKey {
case dappUrl
case category
case discoverImageUrl
case hasZoomDiscoverImage
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

if let urlString = try container.decodeIfPresent(String.self, forKey: .dappUrl) { dappUrl = URL(string: urlString) } else { dappUrl = nil }
if let urlString = try container.decodeIfPresent(String.self, forKey: .discoverImageUrl) { discoverImageUrl = URL(string: urlString) } else { discoverImageUrl = nil }
category = try container.decodeIfPresent([String].self, forKey: .category)
hasZoomDiscoverImage = try container.decodeIfPresent(Bool.self, forKey: .hasZoomDiscoverImage)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(dappUrl?.absoluteString, forKey: .dappUrl)
try container.encode(category, forKey: .category)
try container.encode(discoverImageUrl?.absoluteString, forKey: .discoverImageUrl)
try container.encode(hasZoomDiscoverImage, forKey: .hasZoomDiscoverImage)
}
public struct ExploreEnvironments: Codable {
let mainnet: ExploreEnvironment
let ghostnet: ExploreEnvironment
}

public struct ExploreEnvironment: Codable {
let blockList: [String]
let model3DAllowList: [String]
let contractAliases: [ExploreItem]
}

public struct ExploreItem: Codable {
Expand Down Expand Up @@ -93,24 +73,61 @@ public struct ExploreItem: Codable {
}
}

public struct RemoteDiscoverItem: Codable {
let dappUrl: URL?
let category: [String]?
let discoverImageUrl: URL?
let hasZoomDiscoverImage: Bool?

enum CodingKeys: String, CodingKey {
case dappUrl
case category
case discoverImageUrl
case hasZoomDiscoverImage
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

if let urlString = try container.decodeIfPresent(String.self, forKey: .dappUrl) { dappUrl = URL(string: urlString) } else { dappUrl = nil }
if let urlString = try container.decodeIfPresent(String.self, forKey: .discoverImageUrl) { discoverImageUrl = URL(string: urlString) } else { discoverImageUrl = nil }
category = try container.decodeIfPresent([String].self, forKey: .category)
hasZoomDiscoverImage = try container.decodeIfPresent(Bool.self, forKey: .hasZoomDiscoverImage)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(dappUrl?.absoluteString, forKey: .dappUrl)
try container.encode(category, forKey: .category)
try container.encode(discoverImageUrl?.absoluteString, forKey: .discoverImageUrl)
try container.encode(hasZoomDiscoverImage, forKey: .hasZoomDiscoverImage)
}
}





public class ExploreService {

private let exploreURL = "https://services.kukaiwallet.workers.dev/v1/explore"
private let exploreURL = "https://services.kukai.app/v2/explore?encode=true"

private let exploreCacheKey = "explore-cahce-key"

private let networkService: NetworkService
private let requestIfService: RequestIfService
private let networkType: TezosNodeClientConfig.NetworkType

public var contractAddressToPrimaryKeyMap: [String: UUID] = [:]
public var items: [UUID: ExploreItem] = [:]

public init(networkService: NetworkService) {
public init(networkService: NetworkService, networkType: TezosNodeClientConfig.NetworkType) {
self.networkService = networkService
self.requestIfService = RequestIfService(networkService: networkService)
self.networkType = networkType

let lastCache = self.requestIfService.lastCache(forKey: exploreCacheKey, responseType: [ExploreItem].self)
processRawData(items: lastCache ?? [])
let lastCache = self.requestIfService.lastCache(forKey: exploreCacheKey, responseType: ExploreResponse.self)
processRawData(item: lastCache, networkType: networkType)
}


Expand All @@ -137,14 +154,25 @@ public class ExploreService {
}
}

private func processRawData(items: [ExploreItem]) {
for (index, item) in items.enumerated() {
private func processRawData(item: ExploreResponse?, networkType: TezosNodeClientConfig.NetworkType) {
guard let exploreItem = item else {
return
}

var exploreItemsToProcess: [ExploreItem] = []
if networkType == .mainnet {
exploreItemsToProcess = exploreItem.environment.mainnet.contractAliases
} else {
exploreItemsToProcess = exploreItem.environment.ghostnet.contractAliases
}

for (index, item) in exploreItemsToProcess.enumerated() {
var temp = item
temp.sortIndex = index
self.items[item.primaryKey] = temp
}

self.processQuickFindList(items: items)
self.processQuickFindList(items: exploreItemsToProcess)
}


Expand All @@ -158,13 +186,13 @@ public class ExploreService {
}

// Request from API, no more frequently than once per day, else read cache
self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.fifteenMinute.rawValue, forKey: exploreCacheKey, responseType: [ExploreItem].self) { [weak self] result in
self.requestIfService.request(url: url, withBody: nil, ifElapsedGreaterThan: RequestIfService.TimeConstants.second.rawValue, forKey: exploreCacheKey, responseType: ExploreResponse.self, isSecure: true) { [weak self] result in
guard let response = try? result.get() else {
completion(Result.failure(result.getFailure()))
return
}

self?.processRawData(items: response)
self?.processRawData(item: response, networkType: self?.networkType ?? .mainnet)

completion(Result.success(true))
}
Expand Down
71 changes: 62 additions & 9 deletions Kukai Mobile/Services/RequestIfService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@

import Foundation
import KukaiCoreSwift
import KukaiCryptoSwift
import OSLog

private struct SecureServiceObject: Codable {
let data: String
let signature: String
}

public class RequestIfService {

Expand Down Expand Up @@ -36,13 +43,32 @@ public class RequestIfService {
Send a request to a URL, only if a given time has passed since last request, else return cached version.
Useful for situations where you don't want to overload a server, enforcing that its only called once per day (for example), while avoiding constantly wrapping functions in date checks
*/
public func request<T: Codable>(url: URL, withBody body: Data?, ifElapsedGreaterThan: TimeInterval, forKey key: String, responseType: T.Type, completion: @escaping ((Result<T, KukaiError>) -> Void)) {

let currentTimestmap = Date().timeIntervalSince1970
let lastObj = DiskService.read(type: StorageObject<T>.self, fromFileName: key)
public func request<T: Codable>(url: URL, withBody body: Data?, ifElapsedGreaterThan: TimeInterval, forKey key: String, responseType: T.Type, isSecure: Bool = false, completion: @escaping ((Result<T, KukaiError>) -> Void)) {

if lastObj == nil || (currentTimestmap - (lastObj?.lastRequested ?? currentTimestmap)) > ifElapsedGreaterThan {
if let validObject = checkIfCached(forKey: key, ifElapsedGreaterThan: ifElapsedGreaterThan, forType: T.self) {

// If we have an object sotred on disk, that was stored within the timeframe, extract and return it
completion(Result.success(validObject))

} else if isSecure {

// Else if its a secure endpoint, fetch, validate, parse and store the data
self.networkService.request(url: url, isPOST: body != nil, withBody: body, forReturnType: SecureServiceObject.self) { [weak self] result in
guard let res = try? result.get() else {
completion(Result.failure(result.getFailure()))
return
}

if let validObject = self?.validate(secureObject: res, responseType: T.self) {
let _ = DiskService.write(encodable: StorageObject(lastRequested: Date().timeIntervalSince1970, storedData: validObject), toFileName: key)
completion(Result.success(validObject))
} else {
completion(Result.failure(KukaiError.unknown(withString: "Unable to parse secure object")))
}
}
} else {

// Else if not secure endpoint, just fetch and store
self.networkService.request(url: url, isPOST: body != nil, withBody: body, forReturnType: T.self) { result in
guard let res = try? result.get() else {
completion(Result.failure(result.getFailure()))
Expand All @@ -52,13 +78,21 @@ public class RequestIfService {
let _ = DiskService.write(encodable: StorageObject(lastRequested: Date().timeIntervalSince1970, storedData: res), toFileName: key)
completion(Result.success(res))
}
}
}

private func checkIfCached<T: Codable>(forKey key: String, ifElapsedGreaterThan: TimeInterval, forType: T.Type) -> T? {
let currentTimestmap = Date().timeIntervalSince1970
let lastObj = DiskService.read(type: StorageObject<T>.self, fromFileName: key)

if lastObj == nil || (currentTimestmap - (lastObj?.lastRequested ?? currentTimestmap)) > ifElapsedGreaterThan {
return nil

} else if let obj = lastObj {
completion(Result.success(obj.storedData))

} else {
completion(Result.failure(KukaiError.unknown()))
return obj.storedData
}

return nil
}

public func lastCache<T: Codable>(forKey key: String, responseType: T.Type) -> T? {
Expand All @@ -69,4 +103,23 @@ public class RequestIfService {
public func delete(key: String) -> Bool {
return DiskService.delete(fileName: key)
}

private func validate<T: Codable>(secureObject: SecureServiceObject, responseType: T.Type) -> T? {
guard let publicKeyData = try? Data(hexString: "d71729958d14ba994b9bf29816f9710bd944d0ed7dc3e5a58a31532ca87e06f6"),
let signatureData = try? Data(hexString: secureObject.signature),
let data = Data(base64Encoded: secureObject.data)
else {
Logger.app.error("RequestIfService unable to setup secure data processing")
return nil
}

let publicKey = PublicKey(publicKeyData.bytes, signingCurve: .ed25519)
let valid = publicKey.verify(message: data.bytes, signature: signatureData.bytes)

if valid {
return try? JSONDecoder().decode(T.self, from: data)
}

return nil
}
}

0 comments on commit 9cf7c05

Please sign in to comment.