Merge pull request #1364 from LukasKorba/1363-Binary-address-book-serialization
[#1363] Binary address book serialization
This commit is contained in:
commit
c90939dc04
|
@ -408,7 +408,8 @@ let package = Package(
|
|||
dependencies: [
|
||||
"Utils",
|
||||
"UIComponents",
|
||||
.product(name: "MnemonicSwift", package: "MnemonicSwift")
|
||||
.product(name: "MnemonicSwift", package: "MnemonicSwift"),
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
|
||||
],
|
||||
path: "Sources/Models"
|
||||
),
|
||||
|
|
|
@ -17,7 +17,8 @@ extension DependencyValues {
|
|||
|
||||
@DependencyClient
|
||||
public struct AddressBookClient {
|
||||
public let allContacts: () async throws -> IdentifiedArrayOf<ABRecord>
|
||||
public let storeContact: (ABRecord) async throws -> IdentifiedArrayOf<ABRecord>
|
||||
public let deleteContact: (ABRecord) async throws -> IdentifiedArrayOf<ABRecord>
|
||||
public let allLocalContacts: () throws -> AddressBookContacts
|
||||
public let syncContacts: (AddressBookContacts?) async throws -> AddressBookContacts
|
||||
public let storeContact: (Contact) throws -> AddressBookContacts
|
||||
public let deleteContact: (Contact) throws -> AddressBookContacts
|
||||
}
|
||||
|
|
|
@ -16,108 +16,322 @@ import Combine
|
|||
import WalletStorage
|
||||
|
||||
extension AddressBookClient: DependencyKey {
|
||||
private enum Constants {
|
||||
static let component = "AddressBookData"
|
||||
}
|
||||
|
||||
public enum AddressBookClientError: Error {
|
||||
case missingEncryptionKey
|
||||
case documentsFolder
|
||||
}
|
||||
|
||||
public static let liveValue: AddressBookClient = Self.live()
|
||||
|
||||
public static func live() -> Self {
|
||||
let latestKnownContacts = CurrentValueSubject<IdentifiedArrayOf<ABRecord>?, Never>(nil)
|
||||
var latestKnownContacts: AddressBookContacts?
|
||||
|
||||
@Dependency(\.remoteStorage) var remoteStorage
|
||||
|
||||
return Self(
|
||||
allContacts: {
|
||||
allLocalContacts: {
|
||||
// return latest known contacts
|
||||
guard latestKnownContacts.value == nil else {
|
||||
if let contacts = latestKnownContacts.value {
|
||||
guard latestKnownContacts == nil else {
|
||||
if let contacts = latestKnownContacts {
|
||||
return contacts
|
||||
} else {
|
||||
return []
|
||||
return .empty
|
||||
}
|
||||
}
|
||||
|
||||
// contacts haven't been loaded from the locale storage yet, do it
|
||||
do {
|
||||
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
throw AddressBookClientError.documentsFolder
|
||||
}
|
||||
let fileURL = documentsDirectory.appendingPathComponent(Constants.component)
|
||||
let encryptedContacts = try Data(contentsOf: fileURL)
|
||||
|
||||
let decryptedContacts = try AddressBookClient.decryptData(encryptedContacts)
|
||||
latestKnownContacts = decryptedContacts
|
||||
|
||||
return decryptedContacts
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
syncContacts: { contacts in
|
||||
// Ensure local contacts are prepared
|
||||
var localContacts: AddressBookContacts
|
||||
|
||||
if let contacts {
|
||||
localContacts = contacts
|
||||
} else {
|
||||
do {
|
||||
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
throw AddressBookClientError.documentsFolder
|
||||
}
|
||||
let fileURL = documentsDirectory.appendingPathComponent(Constants.component)
|
||||
let encryptedContacts = try Data(contentsOf: fileURL)
|
||||
|
||||
let decryptedContacts = try AddressBookClient.decryptData(encryptedContacts)
|
||||
localContacts = decryptedContacts
|
||||
|
||||
return decryptedContacts
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// contacts haven't been loaded from the remote storage yet, do it
|
||||
// do {
|
||||
// let data = try await remoteStorage.loadAddressBookContacts()
|
||||
//
|
||||
// let storedContacts = try AddressBookClient.decryptData(data)
|
||||
// latestKnownContacts.value = storedContacts
|
||||
//
|
||||
// return storedContacts
|
||||
// } catch RemoteStorageClient.RemoteStorageError.fileDoesntExist {
|
||||
// return []
|
||||
// } catch {
|
||||
// throw error
|
||||
// }
|
||||
return []
|
||||
let data = try remoteStorage.loadAddressBookContacts()
|
||||
let remoteContacts = try AddressBookClient.decryptData(data)
|
||||
|
||||
// Ensure remote contacts are prepared
|
||||
|
||||
// Merge strategy
|
||||
print("__LD SYNCING CONTACTS...")
|
||||
print("__LD localContacts \(localContacts)")
|
||||
print("__LD remoteContacts \(remoteContacts)")
|
||||
|
||||
var syncedContacts = localContacts
|
||||
|
||||
// TBD
|
||||
|
||||
return syncedContacts
|
||||
},
|
||||
storeContact: {
|
||||
var contacts = latestKnownContacts.value ?? []
|
||||
|
||||
var abContacts = latestKnownContacts ?? AddressBookContacts.empty
|
||||
|
||||
// if already exists, remove it
|
||||
if contacts.contains($0) {
|
||||
contacts.remove($0)
|
||||
if abContacts.contacts.contains($0) {
|
||||
abContacts.contacts.remove($0)
|
||||
}
|
||||
|
||||
contacts.append($0)
|
||||
abContacts.contacts.append($0)
|
||||
|
||||
// push encrypted data to the remote storage
|
||||
//try await remoteStorage.storeAddressBookContacts(AddressBookClient.encryptContacts(contacts))
|
||||
// TODO: FIXME
|
||||
// encrypt data
|
||||
let encryptedContacts = try AddressBookClient.encryptContacts(abContacts)
|
||||
//let decryptedContacts = try AddressBookClient.decryptData(encryptedContacts)
|
||||
|
||||
// store encrypted data to the local storage
|
||||
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
throw AddressBookClientError.documentsFolder
|
||||
}
|
||||
let fileURL = documentsDirectory.appendingPathComponent(Constants.component)
|
||||
try encryptedContacts.write(to: fileURL)
|
||||
|
||||
// store encrypted data to the remote storage
|
||||
try remoteStorage.storeAddressBookContacts(encryptedContacts)
|
||||
|
||||
// update the latest known contacts
|
||||
latestKnownContacts.value = contacts
|
||||
latestKnownContacts = abContacts
|
||||
|
||||
return contacts
|
||||
return abContacts
|
||||
},
|
||||
deleteContact: {
|
||||
var contacts = latestKnownContacts.value ?? []
|
||||
var abContacts = latestKnownContacts ?? AddressBookContacts.empty
|
||||
|
||||
// if it doesn't exist, do nothing
|
||||
guard contacts.contains($0) else {
|
||||
return contacts
|
||||
guard abContacts.contacts.contains($0) else {
|
||||
return abContacts
|
||||
}
|
||||
|
||||
contacts.remove($0)
|
||||
abContacts.contacts.remove($0)
|
||||
|
||||
// push encrypted data to the remote storage
|
||||
//try await remoteStorage.storeAddressBookContacts(AddressBookClient.encryptContacts(contacts))
|
||||
// encrypt data
|
||||
let encryptedContacts = try AddressBookClient.encryptContacts(abContacts)
|
||||
|
||||
// store encrypted data to the local storage
|
||||
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
throw AddressBookClientError.documentsFolder
|
||||
}
|
||||
let fileURL = documentsDirectory.appendingPathComponent(Constants.component)
|
||||
try encryptedContacts.write(to: fileURL)
|
||||
|
||||
// store encrypted data to the remote storage
|
||||
try remoteStorage.storeAddressBookContacts(encryptedContacts)
|
||||
|
||||
// update the latest known contacts
|
||||
latestKnownContacts.value = contacts
|
||||
latestKnownContacts = abContacts
|
||||
|
||||
return contacts
|
||||
return abContacts
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private static func encryptContacts(_ contacts: IdentifiedArrayOf<ABRecord>) throws -> Data {
|
||||
private static func encryptContacts(_ abContacts: AddressBookContacts) throws -> Data {
|
||||
@Dependency(\.walletStorage) var walletStorage
|
||||
|
||||
guard let encryptionKey = try? walletStorage.exportAddressBookKey() else {
|
||||
throw AddressBookClient.AddressBookClientError.missingEncryptionKey
|
||||
}
|
||||
|
||||
// TODO: str4d
|
||||
// guard let encryptionKeys = try? walletStorage.exportAddressBookEncryptionKeys() else {
|
||||
// throw AddressBookClient.AddressBookClientError.missingEncryptionKey
|
||||
// }
|
||||
|
||||
// here you have an array of all contacts
|
||||
// you also have a key from the keychain
|
||||
var data = Data()
|
||||
|
||||
return Data()
|
||||
}
|
||||
|
||||
private static func decryptData(_ data: Data) throws -> IdentifiedArrayOf<ABRecord> {
|
||||
@Dependency(\.walletStorage) var walletStorage
|
||||
// Serialize `version`
|
||||
data.append(contentsOf: intToBytes(abContacts.version))
|
||||
|
||||
guard let encryptionKey = try? walletStorage.exportAddressBookKey() else {
|
||||
throw AddressBookClient.AddressBookClientError.missingEncryptionKey
|
||||
// Serialize `lastUpdated`
|
||||
data.append(contentsOf: AddressBookClient.serializeDate(Date()))
|
||||
|
||||
// Serialize `contacts.count`
|
||||
data.append(contentsOf: intToBytes(abContacts.contacts.count))
|
||||
|
||||
// Serialize `contacts`
|
||||
abContacts.contacts.forEach { contact in
|
||||
let serializedContact = serializeContact(contact)
|
||||
data.append(serializedContact)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
private static func decryptData(_ data: Data) throws -> AddressBookContacts {
|
||||
@Dependency(\.walletStorage) var walletStorage
|
||||
|
||||
// TODO: str4d
|
||||
// guard let encryptionKeys = try? walletStorage.exportAddressBookEncryptionKeys() else {
|
||||
// throw AddressBookClient.AddressBookClientError.missingEncryptionKey
|
||||
// }
|
||||
|
||||
// here you have the encrypted data from the cloud, the blob
|
||||
// you also have a key from the keychain
|
||||
|
||||
var offset = 0
|
||||
|
||||
return []
|
||||
// Deserialize `version`
|
||||
let versionBytes = data.subdata(in: offset..<(offset + MemoryLayout<Int>.size))
|
||||
offset += MemoryLayout<Int>.size
|
||||
|
||||
guard let version = AddressBookClient.bytesToInt(Array(versionBytes)) else {
|
||||
return .empty
|
||||
}
|
||||
|
||||
guard version == AddressBookContacts.Constants.version else {
|
||||
return .empty
|
||||
}
|
||||
|
||||
// Deserialize `lastUpdated`
|
||||
guard let lastUpdated = AddressBookClient.deserializeDate(from: data, at: &offset) else {
|
||||
return .empty
|
||||
}
|
||||
|
||||
// Deserialize `contactsCount`
|
||||
let contactsCountBytes = data.subdata(in: offset..<(offset + MemoryLayout<Int>.size))
|
||||
offset += MemoryLayout<Int>.size
|
||||
|
||||
guard let contactsCount = AddressBookClient.bytesToInt(Array(contactsCountBytes)) else {
|
||||
return .empty
|
||||
}
|
||||
|
||||
var contacts: [Contact] = []
|
||||
for _ in 0..<contactsCount {
|
||||
if let contact = AddressBookClient.deserializeContact(from: data, at: &offset) {
|
||||
contacts.append(contact)
|
||||
}
|
||||
}
|
||||
|
||||
let abContacts = AddressBookContacts(
|
||||
lastUpdated: lastUpdated,
|
||||
version: AddressBookContacts.Constants.version,
|
||||
contacts: IdentifiedArrayOf(uniqueElements: contacts)
|
||||
)
|
||||
|
||||
return abContacts
|
||||
}
|
||||
|
||||
private static func serializeContact(_ contact: Contact) -> Data {
|
||||
var data = Data()
|
||||
|
||||
// Serialize `lastUpdated`
|
||||
data.append(contentsOf: AddressBookClient.serializeDate(contact.lastUpdated))
|
||||
|
||||
// Serialize `address` (length + UTF-8 bytes)
|
||||
let addressBytes = stringToBytes(contact.id)
|
||||
data.append(contentsOf: intToBytes(addressBytes.count))
|
||||
data.append(contentsOf: addressBytes)
|
||||
|
||||
// Serialize `name` (length + UTF-8 bytes)
|
||||
let nameBytes = stringToBytes(contact.name)
|
||||
data.append(contentsOf: intToBytes(nameBytes.count))
|
||||
data.append(contentsOf: nameBytes)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
private static func deserializeContact(from data: Data, at offset: inout Int) -> Contact? {
|
||||
// Deserialize `lastUpdated`
|
||||
guard let lastUpdated = AddressBookClient.deserializeDate(from: data, at: &offset) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deserialize `address`
|
||||
guard let address = readString(from: data, at: &offset) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deserialize `name`
|
||||
guard let name = readString(from: data, at: &offset) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Contact(address: address, name: name, lastUpdated: lastUpdated)
|
||||
}
|
||||
|
||||
private static func stringToBytes(_ string: String) -> [UInt8] {
|
||||
return Array(string.utf8)
|
||||
}
|
||||
|
||||
private static func bytesToString(_ bytes: [UInt8]) -> String? {
|
||||
return String(bytes: bytes, encoding: .utf8)
|
||||
}
|
||||
|
||||
private static func intToBytes(_ value: Int) -> [UInt8] {
|
||||
withUnsafeBytes(of: value.bigEndian, Array.init)
|
||||
}
|
||||
|
||||
private static func bytesToInt(_ bytes: [UInt8]) -> Int? {
|
||||
guard bytes.count == MemoryLayout<Int>.size else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bytes.withUnsafeBytes {
|
||||
$0.load(as: Int.self).bigEndian
|
||||
}
|
||||
}
|
||||
|
||||
private static func serializeDate(_ date: Date) -> [UInt8] {
|
||||
// Convert Date to Unix time (number of seconds since 1970)
|
||||
let timestamp = Int(date.timeIntervalSince1970)
|
||||
|
||||
// Convert the timestamp to bytes
|
||||
return AddressBookClient.intToBytes(timestamp)
|
||||
}
|
||||
|
||||
private static func deserializeDate(from data: Data, at offset: inout Int) -> Date? {
|
||||
// Extract the bytes for the timestamp (assume it's stored as an Int)
|
||||
let timestampBytes = data.subdata(in: offset..<(offset + MemoryLayout<Int>.size))
|
||||
offset += MemoryLayout<Int>.size
|
||||
|
||||
// Convert the bytes back to an Int
|
||||
guard let timestamp = AddressBookClient.bytesToInt(Array(timestampBytes)) else { return nil }
|
||||
|
||||
// Convert the timestamp back to a Date
|
||||
return Date(timeIntervalSince1970: TimeInterval(timestamp))
|
||||
}
|
||||
|
||||
// Helper function to read a string from the data using a length prefix
|
||||
private static func readString(from data: Data, at offset: inout Int) -> String? {
|
||||
// Read the length first (assumes the length is stored as an Int)
|
||||
let lengthBytes = data.subdata(in: offset..<(offset + MemoryLayout<Int>.size))
|
||||
offset += MemoryLayout<Int>.size
|
||||
guard let length = AddressBookClient.bytesToInt(Array(lengthBytes)), length > 0 else { return nil }
|
||||
|
||||
// Read the string bytes
|
||||
let stringBytes = data.subdata(in: offset..<(offset + length))
|
||||
offset += length
|
||||
return AddressBookClient.bytesToString(Array(stringBytes))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,6 @@ extension DependencyValues {
|
|||
|
||||
@DependencyClient
|
||||
public struct RemoteStorageClient {
|
||||
public let loadAddressBookContacts: () async throws -> Data
|
||||
public let storeAddressBookContacts: (Data) async throws -> Void
|
||||
public let loadAddressBookContacts: () throws -> Data
|
||||
public let storeAddressBookContacts: (Data) throws -> Void
|
||||
}
|
||||
|
|
|
@ -34,9 +34,7 @@ extension RemoteStorageClient: DependencyKey {
|
|||
throw RemoteStorageError.fileDoesntExist
|
||||
}
|
||||
|
||||
return try await Task {
|
||||
return try Data(contentsOf: containerURL)
|
||||
}.value
|
||||
return try Data(contentsOf: containerURL)
|
||||
},
|
||||
storeAddressBookContacts: { data in
|
||||
let fileManager = FileManager.default
|
||||
|
@ -45,9 +43,7 @@ extension RemoteStorageClient: DependencyKey {
|
|||
throw RemoteStorageError.containerURL
|
||||
}
|
||||
|
||||
try await Task {
|
||||
try data.write(to: containerURL)
|
||||
}.value
|
||||
try data.write(to: containerURL)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import Models
|
|||
public struct WalletStorage {
|
||||
public enum Constants {
|
||||
public static let zcashStoredWallet = "zcashStoredWallet"
|
||||
public static let zcashStoredAdressBookKey = "zcashStoredAdressBookKey"
|
||||
public static let zcashStoredAdressBookEncryptionKeys = "zcashStoredAdressBookEncryptionKeys"
|
||||
/// Versioning of the stored data
|
||||
public static let zcashKeychainVersion = 1
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ public struct WalletStorage {
|
|||
|
||||
public enum WalletStorageError: Error {
|
||||
case alreadyImported
|
||||
case uninitializedAddressBookKey
|
||||
case uninitializedAddressBookEncryptionKeys
|
||||
case uninitializedWallet
|
||||
case storageError(Error)
|
||||
case unsupportedVersion(Int)
|
||||
|
@ -140,13 +140,13 @@ public struct WalletStorage {
|
|||
deleteData(forKey: Constants.zcashStoredWallet)
|
||||
}
|
||||
|
||||
public func importAddressBookKey(_ key: String) throws {
|
||||
public func importAddressBookEncryptionKeys(_ keys: AddressBookEncryptionKeys) throws {
|
||||
do {
|
||||
guard let data = try encode(object: key) else {
|
||||
guard let data = try encode(object: keys) else {
|
||||
throw KeychainError.encoding
|
||||
}
|
||||
|
||||
try setData(data, forKey: Constants.zcashStoredAdressBookKey)
|
||||
try setData(data, forKey: Constants.zcashStoredAdressBookEncryptionKeys)
|
||||
} catch KeychainError.duplicate {
|
||||
throw WalletStorageError.alreadyImported
|
||||
} catch {
|
||||
|
@ -154,13 +154,13 @@ public struct WalletStorage {
|
|||
}
|
||||
}
|
||||
|
||||
public func exportAddressBookKey() throws -> String {
|
||||
guard let data = data(forKey: Constants.zcashStoredAdressBookKey) else {
|
||||
throw WalletStorageError.uninitializedAddressBookKey
|
||||
public func exportAddressBookEncryptionKeys() throws -> AddressBookEncryptionKeys {
|
||||
guard let data = data(forKey: Constants.zcashStoredAdressBookEncryptionKeys) else {
|
||||
throw WalletStorageError.uninitializedAddressBookEncryptionKeys
|
||||
}
|
||||
|
||||
guard let wallet = try decode(json: data, as: String.self) else {
|
||||
throw WalletStorageError.uninitializedAddressBookKey
|
||||
guard let wallet = try decode(json: data, as: AddressBookEncryptionKeys.self) else {
|
||||
throw WalletStorageError.uninitializedAddressBookEncryptionKeys
|
||||
}
|
||||
|
||||
return wallet
|
||||
|
|
|
@ -77,6 +77,6 @@ public struct WalletStorageClient {
|
|||
|
||||
// TODO: str4d
|
||||
// not sure what format the key is, for now I made it a String
|
||||
public var importAddressBookKey: (String) throws -> Void
|
||||
public var exportAddressBookKey: () throws -> String
|
||||
public var importAddressBookEncryptionKeys: (AddressBookEncryptionKeys) throws -> Void
|
||||
public var exportAddressBookEncryptionKeys: () throws -> AddressBookEncryptionKeys
|
||||
}
|
||||
|
|
|
@ -39,11 +39,11 @@ extension WalletStorageClient: DependencyKey {
|
|||
nukeWallet: {
|
||||
walletStorage.nukeWallet()
|
||||
},
|
||||
importAddressBookKey: { key in
|
||||
try walletStorage.importAddressBookKey(key)
|
||||
importAddressBookEncryptionKeys: { keys in
|
||||
try walletStorage.importAddressBookEncryptionKeys(keys)
|
||||
},
|
||||
exportAddressBookKey: {
|
||||
try walletStorage.exportAddressBookKey()
|
||||
exportAddressBookEncryptionKeys: {
|
||||
try walletStorage.exportAddressBookEncryptionKeys()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ extension WalletStorageClient: TestDependencyKey {
|
|||
updateBirthday: unimplemented("\(Self.self).updateBirthday", placeholder: {}()),
|
||||
markUserPassedPhraseBackupTest: unimplemented("\(Self.self).markUserPassedPhraseBackupTest", placeholder: {}()),
|
||||
nukeWallet: unimplemented("\(Self.self).nukeWallet", placeholder: {}()),
|
||||
importAddressBookKey: unimplemented("\(Self.self).importAddressBookKey", placeholder: {}()),
|
||||
exportAddressBookKey: unimplemented("\(Self.self).exportAddressBookKey", placeholder: "")
|
||||
importAddressBookEncryptionKeys: unimplemented("\(Self.self).importAddressBookEncryptionKeys", placeholder: {}()),
|
||||
exportAddressBookEncryptionKeys: unimplemented("\(Self.self).exportAddressBookEncryptionKeys", placeholder: .empty)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ extension WalletStorageClient {
|
|||
updateBirthday: { _ in },
|
||||
markUserPassedPhraseBackupTest: { _ in },
|
||||
nukeWallet: { },
|
||||
importAddressBookKey: { _ in },
|
||||
exportAddressBookKey: { "" }
|
||||
importAddressBookEncryptionKeys: { _ in },
|
||||
exportAddressBookEncryptionKeys: { .empty }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ public struct AddressBook {
|
|||
|
||||
public var address = ""
|
||||
public var addressAlreadyExists = false
|
||||
@Shared(.inMemory(.addressBookRecords)) public var addressBookRecords: IdentifiedArrayOf<ABRecord> = []
|
||||
@Shared(.inMemory(.addressBookContacts)) public var addressBookContacts: AddressBookContacts = .empty
|
||||
@Presents public var alert: AlertState<Action>?
|
||||
public var deleteIdToConfirm: String?
|
||||
public var destination: Destination?
|
||||
|
@ -93,8 +93,8 @@ public struct AddressBook {
|
|||
case deleteId(String)
|
||||
case deleteIdConfirmed
|
||||
case editId(String)
|
||||
case fetchedABRecords(IdentifiedArrayOf<ABRecord>)
|
||||
case fetchABRecordsRequested
|
||||
case fetchedABContacts(AddressBookContacts, Bool)
|
||||
case fetchABContactsRequested
|
||||
case onAppear
|
||||
case checkDuplicates
|
||||
case saveButtonTapped
|
||||
|
@ -132,7 +132,7 @@ public struct AddressBook {
|
|||
state.addressAlreadyExists = false
|
||||
state.isAddressFocused = false
|
||||
state.isNameFocused = false
|
||||
return .send(.fetchABRecordsRequested)
|
||||
return .send(.fetchABContactsRequested)
|
||||
|
||||
case .alert(.presented(let action)):
|
||||
return Effect.send(action)
|
||||
|
@ -155,11 +155,11 @@ public struct AddressBook {
|
|||
case .checkDuplicates:
|
||||
state.nameAlreadyExists = false
|
||||
state.addressAlreadyExists = false
|
||||
for record in state.addressBookRecords {
|
||||
if record.name == state.name && state.name != state.originalName {
|
||||
for contact in state.addressBookContacts.contacts {
|
||||
if contact.name == state.name && state.name != state.originalName {
|
||||
state.nameAlreadyExists = true
|
||||
}
|
||||
if record.id == state.address && state.address != state.originalAddress {
|
||||
if contact.id == state.address && state.address != state.originalAddress {
|
||||
state.addressAlreadyExists = true
|
||||
}
|
||||
}
|
||||
|
@ -199,18 +199,18 @@ public struct AddressBook {
|
|||
return .none
|
||||
}
|
||||
|
||||
let record = state.addressBookRecords.first {
|
||||
let contact = state.addressBookContacts.contacts.first {
|
||||
$0.id == deleteIdToConfirm
|
||||
}
|
||||
if let record {
|
||||
return .run { send in
|
||||
do {
|
||||
let contacts = try await addressBook.deleteContact(record)
|
||||
await send(.fetchedABRecords(contacts))
|
||||
await send(.updateDestination(nil))
|
||||
} catch {
|
||||
// TODO: FIXME
|
||||
}
|
||||
if let contact {
|
||||
do {
|
||||
let contacts = try addressBook.deleteContact(contact)
|
||||
return .concatenate(
|
||||
.send(.fetchedABContacts(contacts, false)),
|
||||
.send(.updateDestination(nil))
|
||||
)
|
||||
} catch {
|
||||
// TODO: FIXME
|
||||
}
|
||||
}
|
||||
return .none
|
||||
|
@ -220,7 +220,7 @@ public struct AddressBook {
|
|||
return .none
|
||||
}
|
||||
|
||||
let record = state.addressBookRecords.first {
|
||||
let record = state.addressBookContacts.contacts.first {
|
||||
$0.id == id
|
||||
}
|
||||
guard let record else {
|
||||
|
@ -237,18 +237,16 @@ public struct AddressBook {
|
|||
return .send(.updateDestination(.add))
|
||||
|
||||
case .saveButtonTapped:
|
||||
// let name = state.name.isEmpty ? "testName" : state.name
|
||||
// let address = state.address.isEmpty ? "testAddress" : state.address
|
||||
return .run { [state] send in
|
||||
do {
|
||||
let contacts = try await addressBook.storeContact(ABRecord(address: state.address, name: state.name))
|
||||
await send(.fetchedABRecords(contacts))
|
||||
await send(.contactStoreSuccess)
|
||||
} catch {
|
||||
// TODO: FIXME
|
||||
print("__LD saveButtonTapped Error: \(error.localizedDescription)")
|
||||
await send(.updateDestination(nil))
|
||||
}
|
||||
do {
|
||||
let abContacts = try addressBook.storeContact(Contact(address: state.address, name: state.name))
|
||||
return .concatenate(
|
||||
.send(.fetchedABContacts(abContacts, false)),
|
||||
.send(.contactStoreSuccess)
|
||||
)
|
||||
} catch {
|
||||
// TODO: FIXME
|
||||
print("__LD saveButtonTapped Error: \(error.localizedDescription)")
|
||||
return .send(.updateDestination(nil))
|
||||
}
|
||||
|
||||
case .contactStoreSuccess:
|
||||
|
@ -258,21 +256,31 @@ public struct AddressBook {
|
|||
state.isNameFocused = false
|
||||
return .send(.updateDestination(nil))
|
||||
|
||||
case .fetchABRecordsRequested:
|
||||
return .run { send in
|
||||
do {
|
||||
let records = try await addressBook.allContacts()
|
||||
await send(.fetchedABRecords(records))
|
||||
print("__LD updateRecords success")
|
||||
} catch {
|
||||
print("__LD updateRecords Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
case .fetchABContactsRequested:
|
||||
do {
|
||||
let abContacts = try addressBook.allLocalContacts()
|
||||
return .send(.fetchedABContacts(abContacts, true))
|
||||
} catch {
|
||||
print("__LD fetchABContactsRequested Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
return .none
|
||||
}
|
||||
|
||||
case let .fetchedABContacts(abContacts, requestToSync):
|
||||
state.addressBookContacts = abContacts
|
||||
if requestToSync {
|
||||
return .run { send in
|
||||
do {
|
||||
let syncedContacts = try await addressBook.syncContacts(abContacts)
|
||||
await send(.fetchedABContacts(syncedContacts, false))
|
||||
} catch {
|
||||
print("__LD syncContacts Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
|
||||
case .fetchedABRecords(let records):
|
||||
state.addressBookRecords = records
|
||||
return .none
|
||||
|
||||
case .updateDestination(let destination):
|
||||
state.destination = destination
|
||||
|
|
|
@ -34,7 +34,7 @@ public struct AddressBookView: View {
|
|||
public var body: some View {
|
||||
WithPerceptionTracking {
|
||||
VStack() {
|
||||
if store.addressBookRecords.isEmpty {
|
||||
if store.addressBookContacts.contacts.isEmpty {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 40) {
|
||||
|
@ -56,7 +56,7 @@ public struct AddressBookView: View {
|
|||
}
|
||||
|
||||
List {
|
||||
ForEach(store.addressBookRecords, id: \.self) { record in
|
||||
ForEach(store.addressBookContacts.contacts, id: \.self) { record in
|
||||
VStack {
|
||||
ContactView(
|
||||
iconText: record.name.initials,
|
||||
|
@ -66,7 +66,7 @@ public struct AddressBookView: View {
|
|||
store.send(.editId(record.id))
|
||||
}
|
||||
|
||||
if let last = store.addressBookRecords.last, last != record {
|
||||
if let last = store.addressBookContacts.contacts.last, last != record {
|
||||
Design.Surfaces.divider.color
|
||||
.frame(height: 1)
|
||||
.padding(.top, 12)
|
||||
|
|
|
@ -285,16 +285,16 @@ extension Root {
|
|||
try mnemonic.isValid(storedWallet.seedPhrase.value())
|
||||
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())
|
||||
|
||||
let addressBookEncryptionKey = try? walletStorage.exportAddressBookKey()
|
||||
if addressBookEncryptionKey == nil {
|
||||
let addressBookEncryptionKeys = try? walletStorage.exportAddressBookEncryptionKeys()
|
||||
if addressBookEncryptionKeys == nil {
|
||||
// TODO: str4d
|
||||
// here you know the encryption key for the address book is missing, we need to generate one
|
||||
|
||||
// here you have `storedWallet.seedPhrase.seedPhrase`, a seed as String
|
||||
|
||||
// once the key is prepared, store it
|
||||
// let key == ""
|
||||
// try walletStorage.importAddressBookKey(key)
|
||||
// let keys == AddressBookEncryptionKeys(key: "")
|
||||
// try walletStorage.importAddressBookEncryptionKeys(keys)
|
||||
}
|
||||
|
||||
return .run { send in
|
||||
|
|
|
@ -29,7 +29,7 @@ public struct SendConfirmation {
|
|||
@ObservableState
|
||||
public struct State: Equatable {
|
||||
public var address: String
|
||||
@Shared(.inMemory(.addressBookRecords)) public var addressBookRecords: IdentifiedArrayOf<ABRecord> = []
|
||||
@Shared(.inMemory(.addressBookContacts)) public var addressBookContacts: AddressBookContacts = .empty
|
||||
public var alias: String?
|
||||
@Presents public var alert: AlertState<Action>?
|
||||
public var amount: Zatoshi
|
||||
|
@ -77,7 +77,7 @@ public struct SendConfirmation {
|
|||
public enum Action: BindableAction, Equatable {
|
||||
case alert(PresentationAction<Action>)
|
||||
case binding(BindingAction<SendConfirmation.State>)
|
||||
case fetchedABRecords(IdentifiedArrayOf<ABRecord>)
|
||||
case fetchedABContacts(AddressBookContacts)
|
||||
case goBackPressed
|
||||
case goBackPressedFromRequestZec
|
||||
case onAppear
|
||||
|
@ -112,22 +112,20 @@ public struct SendConfirmation {
|
|||
switch action {
|
||||
case .onAppear:
|
||||
state.isTransparentAddress = derivationTool.isTransparentAddress(state.address, zcashSDKEnvironment.network.networkType)
|
||||
return .run { send in
|
||||
do {
|
||||
let records = try await addressBook.allContacts()
|
||||
await send(.fetchedABRecords(records))
|
||||
print("__LD updateRecords success")
|
||||
} catch {
|
||||
print("__LD updateRecords Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
do {
|
||||
let abContacts = try addressBook.allLocalContacts()
|
||||
return .send(.fetchedABContacts(abContacts))
|
||||
} catch {
|
||||
print("__LD fetchABContactsRequested Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
return .none
|
||||
}
|
||||
|
||||
case .fetchedABRecords(let records):
|
||||
state.addressBookRecords = records
|
||||
for record in state.addressBookRecords {
|
||||
if record.id == state.address {
|
||||
state.alias = record.name
|
||||
|
||||
case .fetchedABContacts(let abContacts):
|
||||
state.addressBookContacts = abContacts
|
||||
for contact in state.addressBookContacts.contacts {
|
||||
if contact.id == state.address {
|
||||
state.alias = contact.name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ public struct SendFlow {
|
|||
public var cancelId = UUID()
|
||||
|
||||
public var addMemoState: Bool
|
||||
@Shared(.inMemory(.addressBookRecords)) public var addressBookRecords: IdentifiedArrayOf<ABRecord> = []
|
||||
@Shared(.inMemory(.addressBookContacts)) public var addressBookContacts: AddressBookContacts = .empty
|
||||
@Presents public var alert: AlertState<Action>?
|
||||
@Shared(.inMemory(.exchangeRate)) public var currencyConversion: CurrencyConversion? = nil
|
||||
public var destination: Destination?
|
||||
|
@ -203,7 +203,7 @@ public struct SendFlow {
|
|||
case currencyUpdated(RedactableString)
|
||||
case dismissAddressBookHint
|
||||
case exchangeRateSetupChanged
|
||||
case fetchedABRecords(IdentifiedArrayOf<ABRecord>)
|
||||
case fetchedABContacts(AddressBookContacts)
|
||||
case memo(MessageEditor.Action)
|
||||
case onAppear
|
||||
case onDisapear
|
||||
|
@ -246,22 +246,20 @@ public struct SendFlow {
|
|||
switch action {
|
||||
case .onAppear:
|
||||
state.memoState.charLimit = zcashSDKEnvironment.memoCharLimit
|
||||
return .merge(
|
||||
.send(.exchangeRateSetupChanged),
|
||||
.run { send in
|
||||
do {
|
||||
let records = try await addressBook.allContacts()
|
||||
await send(.fetchedABRecords(records))
|
||||
print("__LD updateRecords success")
|
||||
} catch {
|
||||
print("__LD updateRecords Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
}
|
||||
)
|
||||
do {
|
||||
let abContacts = try addressBook.allLocalContacts()
|
||||
return .merge(
|
||||
.send(.exchangeRateSetupChanged),
|
||||
.send(.fetchedABContacts(abContacts))
|
||||
)
|
||||
} catch {
|
||||
// TODO: FIXME
|
||||
print("__LD fetchABContactsRequested Error: \(error.localizedDescription)")
|
||||
return .send(.exchangeRateSetupChanged)
|
||||
}
|
||||
|
||||
case .fetchedABRecords(let records):
|
||||
state.addressBookRecords = records
|
||||
case .fetchedABContacts(let abContacts):
|
||||
state.addressBookContacts = abContacts
|
||||
return .none
|
||||
|
||||
case .onDisapear:
|
||||
|
@ -446,8 +444,8 @@ public struct SendFlow {
|
|||
state.isNotAddressInAddressBook = state.isValidAddress
|
||||
var isNotAddressInAddressBook = state.isNotAddressInAddressBook
|
||||
if state.isValidAddress {
|
||||
for record in state.addressBookRecords {
|
||||
if record.id == state.address.data {
|
||||
for contact in state.addressBookContacts.contacts {
|
||||
if contact.id == state.address.data {
|
||||
state.isNotAddressInAddressBook = false
|
||||
isNotAddressInAddressBook = false
|
||||
break
|
||||
|
|
|
@ -125,7 +125,7 @@ public struct SendFlowView: View {
|
|||
)
|
||||
.keyboardType(.decimalPad)
|
||||
.focused($isCurrencyFocused)
|
||||
.padding(.top, 26)
|
||||
.padding(.top, 23)
|
||||
.disabled(store.currencyConversion == nil)
|
||||
.opacity(store.currencyConversion == nil ? 0.5 : 1.0)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ public struct TransactionList {
|
|||
|
||||
@ObservableState
|
||||
public struct State: Equatable {
|
||||
@Shared(.inMemory(.addressBookRecords)) public var addressBookRecords: IdentifiedArrayOf<ABRecord> = []
|
||||
@Shared(.inMemory(.addressBookContacts)) public var addressBookContacts: AddressBookContacts = .empty
|
||||
public var latestMinedHeight: BlockHeight?
|
||||
public var latestTransactionId = ""
|
||||
public var latestTransactionList: [TransactionState] = []
|
||||
|
@ -39,7 +39,7 @@ public struct TransactionList {
|
|||
|
||||
public enum Action: Equatable {
|
||||
case copyToPastboard(RedactableString)
|
||||
case fetchedABRecords(IdentifiedArrayOf<ABRecord>)
|
||||
case fetchedABContacts(AddressBookContacts)
|
||||
case foundTransactions
|
||||
case memosFor([Memo], String)
|
||||
case onAppear
|
||||
|
@ -67,6 +67,14 @@ public struct TransactionList {
|
|||
switch action {
|
||||
case .onAppear:
|
||||
state.requiredTransactionConfirmations = zcashSDKEnvironment.requiredTransactionConfirmations
|
||||
do {
|
||||
let abContacts = try addressBook.allLocalContacts()
|
||||
state.addressBookContacts = abContacts
|
||||
} catch {
|
||||
print("__LD fetchABContactsRequested Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
|
||||
return .merge(
|
||||
.publisher {
|
||||
sdkSynchronizer.stateStream()
|
||||
|
@ -89,27 +97,17 @@ public struct TransactionList {
|
|||
if let transactions = try? await sdkSynchronizer.getAllTransactions() {
|
||||
await send(.updateTransactionList(transactions))
|
||||
}
|
||||
},
|
||||
.run { send in
|
||||
do {
|
||||
let records = try await addressBook.allContacts()
|
||||
await send(.fetchedABRecords(records))
|
||||
print("__LD updateRecords success")
|
||||
} catch {
|
||||
print("__LD updateRecords Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
case .fetchedABRecords(let records):
|
||||
state.addressBookRecords = records
|
||||
case .fetchedABContacts(let abContacts):
|
||||
state.addressBookContacts = abContacts
|
||||
let modifiedTransactionState = state.transactionList.map { transaction in
|
||||
var copiedTransaction = transaction
|
||||
|
||||
copiedTransaction.isInAddressBook = false
|
||||
for record in state.addressBookRecords {
|
||||
if record.id == transaction.address {
|
||||
for contact in state.addressBookContacts.contacts {
|
||||
if contact.id == transaction.address {
|
||||
copiedTransaction.isInAddressBook = true
|
||||
break
|
||||
}
|
||||
|
@ -189,8 +187,8 @@ public struct TransactionList {
|
|||
|
||||
// in address book
|
||||
copiedTransaction.isInAddressBook = false
|
||||
for record in state.addressBookRecords {
|
||||
if record.id == transaction.address {
|
||||
for contact in state.addressBookContacts.contacts {
|
||||
if contact.id == transaction.address {
|
||||
copiedTransaction.isInAddressBook = true
|
||||
break
|
||||
}
|
||||
|
@ -220,8 +218,8 @@ public struct TransactionList {
|
|||
if let index = state.transactionList.index(id: id) {
|
||||
if state.transactionList[index].isExpanded {
|
||||
state.transactionList[index].isAddressExpanded = true
|
||||
for record in state.addressBookRecords {
|
||||
if record.id == state.transactionList[index].address {
|
||||
for contact in state.addressBookContacts.contacts {
|
||||
if contact.id == state.transactionList[index].address {
|
||||
state.transactionList[index].isInAddressBook = true
|
||||
break
|
||||
}
|
||||
|
|
|
@ -12,6 +12,6 @@ public extension String {
|
|||
static let sensitiveContent = "udHideBalances"
|
||||
static let walletStatus = "sharedStateKey_walletStatus"
|
||||
static let flexaAccountId = "sharedStateKey_flexaAccountId"
|
||||
static let addressBookRecords = "sharedStateKey_addressBookRecords"
|
||||
static let addressBookContacts = "sharedStateKey_addressBookContacts"
|
||||
static let toast = "sharedStateKey_toast"
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
//
|
||||
// ABRecord.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 05-28-2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ABRecord: Equatable, Codable, Identifiable, Hashable {
|
||||
public let id: String
|
||||
public var name: String
|
||||
public let timestamp: Date
|
||||
|
||||
public init(address: String, name: String, timestamp: Date = Date()) {
|
||||
self.id = address
|
||||
self.name = name
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// AddressBookContacts.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 09-30-2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
public struct AddressBookContacts: Equatable, Codable {
|
||||
public enum Constants {
|
||||
public static let version = 1
|
||||
}
|
||||
|
||||
public let lastUpdated: Date
|
||||
public let version: Int
|
||||
public var contacts: IdentifiedArrayOf<Contact>
|
||||
|
||||
public init(lastUpdated: Date, version: Int, contacts: IdentifiedArrayOf<Contact>) {
|
||||
self.lastUpdated = lastUpdated
|
||||
self.version = version
|
||||
self.contacts = contacts
|
||||
}
|
||||
}
|
||||
|
||||
public extension AddressBookContacts {
|
||||
static let empty = AddressBookContacts(lastUpdated: .distantPast, version: Constants.version, contacts: [])
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// AddressBookEncryptionKeys.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 09-30-2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Representation of the address book encryption keys
|
||||
public struct AddressBookEncryptionKeys: Codable, Equatable {
|
||||
public let key: String
|
||||
|
||||
public init(key: String) {
|
||||
self.key = key
|
||||
}
|
||||
}
|
||||
|
||||
extension AddressBookEncryptionKeys {
|
||||
public static let empty = Self(
|
||||
key: ""
|
||||
)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// Contact.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 05-28-2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Contact: Equatable, Codable, Identifiable, Hashable {
|
||||
public let id: String
|
||||
public var name: String
|
||||
public let lastUpdated: Date
|
||||
|
||||
public init(address: String, name: String, lastUpdated: Date = Date()) {
|
||||
self.id = address
|
||||
self.name = name
|
||||
self.lastUpdated = lastUpdated
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue