Skip to content

Commit

Permalink
- convert walletmetadata and list to classes so we can use pass by re…
Browse files Browse the repository at this point in the history
…ference

- update caching/naming/deleting logic to use references to arrays in a generic way so that we can share bits of similar logic
- add some ledger specific helpers
- add new tests
  • Loading branch information
simonmcl committed Aug 13, 2024
1 parent a749cc5 commit 2416714
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 56 deletions.
43 changes: 33 additions & 10 deletions Sources/KukaiCoreSwift/Models/WalletMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -85,7 +85,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 }
}
Expand Down Expand Up @@ -113,8 +113,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]
Expand All @@ -131,8 +131,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) {
Expand All @@ -142,8 +142,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) {
Expand Down Expand Up @@ -224,14 +224,30 @@ 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)
}
}





/// 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 hdWalletGroupName: String?
public var walletNickname: String?
Expand All @@ -246,6 +262,7 @@ public struct WalletMetadata: Codable, Hashable {
public var isWatchOnly: Bool
public var bas58EncodedPublicKey: String
public var backedUp: Bool
public var customDerivationPath: String?

public func hasMainnetDomain() -> Bool {
return (mainnetDomains ?? []).count > 0
Expand Down Expand Up @@ -287,7 +304,12 @@ 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 func childCountExcludingCustomDerivationPaths() -> Int {
let excluded = children.filter { $0.customDerivationPath == nil }
return excluded.count
}

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, customDerivationPath: String?) {
self.address = address
self.hdWalletGroupName = hdWalletGroupName
self.walletNickname = walletNickname
Expand All @@ -302,6 +324,7 @@ public struct WalletMetadata: Codable, Hashable {
self.isWatchOnly = isWatchOnly
self.bas58EncodedPublicKey = bas58EncodedPublicKey
self.backedUp = backedUp
self.customDerivationPath = customDerivationPath
}

public static func == (lhs: WalletMetadata, rhs: WalletMetadata) -> Bool {
Expand Down
107 changes: 75 additions & 32 deletions Sources/KukaiCoreSwift/Services/WalletCacheService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public class WalletCacheService {
- Parameter childOfIndex: An optional `Int` to denote the index of the HD wallet that this wallet is a child of
- Returns: Bool, indicating if the storage was successful or not
*/
public func cache<T: Wallet>(wallet: T, childOfIndex: Int?, backedUp: Bool) throws {
public func cache<T: Wallet>(wallet: T, childOfIndex: Int?, backedUp: Bool, customDerivationPath: String? = nil) throws {
guard let existingWallets = readWalletsFromDiskAndDecrypt() else {
Logger.walletCache.error("cache - Unable to cache wallet, as can't decrypt existing wallets")
throw WalletCacheError.unableToDecrypt
Expand All @@ -99,47 +99,96 @@ public class WalletCacheService {
var newWallets = existingWallets
newWallets[wallet.address] = wallet

var newMetadata = readMetadataFromDiskAndDecrypt()
let newMetadata = readMetadataFromDiskAndDecrypt()
var array = metadataArray(forType: wallet.type, fromMeta: newMetadata)

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, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, type: wallet.type, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath))

} else if let _ = wallet as? HDWallet {
} else if wallet.type == .hd || wallet.type == .ledger {

// 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 Wallet "
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, hdWalletGroupName: "\(groupNameStart)\(newNumber)", walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath))

} 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, 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, customDerivationPath: customDerivationPath))

} 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, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath))
}


// Update wallet metadata array, and then commit to disk
updateMetadataArray(forType: wallet.type, withNewArray: array, frorMeta: 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], frorMeta: WalletMetadataList) {
switch forType {
case .regular:
frorMeta.linearWallets = withNewArray
case .regularShifted:
frorMeta.linearWallets = withNewArray
case .hd:
frorMeta.hdWallets = withNewArray
case .social:
frorMeta.socialWallets = withNewArray
case .ledger:
frorMeta.ledgerWallets = withNewArray
}
}

private func removeNewAddressFromWatchListIfExists(_ address: String, list: WalletMetadataList) {
if let _ = list.watchWallets.first(where: { $0.address == address }) {
let _ = deleteWatchWallet(address: address)
Expand All @@ -150,7 +199,7 @@ public class WalletCacheService {
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.addresses().first(where: { $0 == metadata.address }) {
Logger.walletCache.error("cacheWatchWallet - Unable to cache wallet, wallet already exists")
Expand All @@ -177,37 +226,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")
Expand All @@ -222,7 +265,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil)
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)
Expand Down
Loading

0 comments on commit 2416714

Please sign in to comment.