Merge pull request #1364 from LukasKorba/1363-Binary-address-book-serialization

[#1363] Binary address book serialization
This commit is contained in:
Lukas Korba 2024-10-04 10:00:22 +02:00 committed by GitHub
commit c90939dc04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 477 additions and 211 deletions

View File

@ -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"
),

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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)
}
)
}

View File

@ -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

View File

@ -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
}

View File

@ -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()
}
)
}

View File

@ -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 }
)
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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
}
}

View File

@ -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: [])
}

View File

@ -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: ""
)
}

View File

@ -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
}
}