secant-ios-wallet/modules/Sources/Features/ServerSetup/ServerSetupStore.swift

249 lines
9.0 KiB
Swift

//
// ServerSetup.swift
// Zashi
//
// Created by Lukáš Korba on 2024-02-07.
//
import Foundation
import ComposableArchitecture
import ZcashLightClientKit
import Generated
import SDKSynchronizer
import UserPreferencesStorage
import ZcashSDKEnvironment
extension LightWalletEndpoint: @retroactive Equatable {
public static func == (lhs: LightWalletEndpoint, rhs: LightWalletEndpoint) -> Bool {
lhs.host == rhs.host
&& lhs.port == rhs.port
&& lhs.streamingCallTimeoutInMillis == rhs.streamingCallTimeoutInMillis
&& lhs.singleCallTimeoutInMillis == rhs.singleCallTimeoutInMillis
&& lhs.secure == rhs.secure
}
}
@Reducer
public struct ServerSetup {
let streamingCallTimeoutInMillis = ZcashSDKEnvironment.ZcashSDKConstants.streamingCallTimeoutInMillis
@ObservableState
public struct State: Equatable {
var activeServer: String
@Presents var alert: AlertState<Action>?
var customServer: String
var isEvaluatingServers = false
var isUpdatingServer = false
var initialServer: String
var network: NetworkType = .mainnet
var selectedServer: String?
var servers: [ZcashSDKEnvironment.Server]
var topKServers: [ZcashSDKEnvironment.Server]
public init(
activeServer: String = "",
customServer: String = "",
isEvaluatingServers: Bool = false,
isUpdatingServer: Bool = false,
initialServer: String = "",
network: NetworkType = .mainnet,
selectedServer: String? = nil,
servers: [ZcashSDKEnvironment.Server] = [],
topKServers: [ZcashSDKEnvironment.Server] = []
) {
self.activeServer = activeServer
self.customServer = customServer
self.isUpdatingServer = isUpdatingServer
self.initialServer = initialServer
self.network = network
self.selectedServer = selectedServer
self.servers = servers
self.topKServers = topKServers
}
}
public enum Action: Equatable, BindableAction {
case alert(PresentationAction<Action>)
case binding(BindingAction<State>)
case evaluatedServers([LightWalletEndpoint])
case evaluateServers
case onAppear
case onDisappear
case refreshServersTapped
case setServerTapped
case someServerTapped(ZcashSDKEnvironment.Server)
case switchFailed(ZcashError)
case switchSucceeded
}
public init() {}
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
@Dependency(\.userStoredPreferences) var userStoredPreferences
public var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .onAppear:
state.network = zcashSDKEnvironment.network.networkType
if !state.topKServers.isEmpty {
let allServers = ZcashSDKEnvironment.servers(for: state.network)
state.servers = allServers.filter {
!state.topKServers.contains($0)
}
} else {
state.servers = ZcashSDKEnvironment.servers(for: state.network)
}
let serverConfig = zcashSDKEnvironment.serverConfig()
if serverConfig.isCustom {
state.initialServer = L10n.ServerSetup.custom
state.customServer = serverConfig.serverString()
} else {
state.initialServer = serverConfig.serverString()
}
state.activeServer = state.initialServer
return state.topKServers.isEmpty ? .send(.evaluateServers) : .none
case .onDisappear:
state.selectedServer = nil
return .none
case .alert(.dismiss):
state.alert = nil
return .none
case .alert:
return .none
case .binding:
return .none
case .evaluateServers:
state.isEvaluatingServers = true
return .run { send in
let kBestServers = await sdkSynchronizer.evaluateBestOf(
ZcashSDKEnvironment.endpoints(),
300.0,
60.0,
100,
3,
.mainnet
)
await send(.evaluatedServers(kBestServers))
}
case .evaluatedServers(let bestServers):
state.isEvaluatingServers = false
state.topKServers = bestServers.map {
if ZcashSDKEnvironment.Server.default.value(for: state.network) == $0.server() {
ZcashSDKEnvironment.Server.default
} else {
ZcashSDKEnvironment.Server.hardcoded("\($0.host):\($0.port)")
}
}
let allServers = ZcashSDKEnvironment.servers(for: state.network)
state.servers = allServers.filter {
!state.topKServers.contains($0)
}
return .none
case .refreshServersTapped:
return .send(.evaluateServers)
case .setServerTapped:
guard state.initialServer != state.selectedServer || state.selectedServer == L10n.ServerSetup.custom else {
return .none
}
state.isUpdatingServer = true
// custom server needs to be stored first
var input = state.selectedServer ?? state.activeServer
if input == L10n.ServerSetup.custom {
input = state.customServer
}
return .run { [input] send in
do {
let endpoint = UserPreferencesStorage.ServerConfig.endpoint(
for: input,
streamingCallTimeoutInMillis: streamingCallTimeoutInMillis
)
guard let endpoint else {
throw ZcashError.synchronizerServerSwitch
}
try await sdkSynchronizer.switchToEndpoint(endpoint)
try await mainQueue.sleep(for: .seconds(1))
await send(.switchSucceeded)
} catch {
await send(.switchFailed(error.toZcashError()))
}
}
case .someServerTapped(let newChange):
state.selectedServer = newChange.value(for: state.network)
return .none
case .switchFailed(let error):
state.isUpdatingServer = false
state.alert = AlertState.endpoindSwitchFailed(error)
return .none
case .switchSucceeded:
state.isUpdatingServer = false
state.initialServer = state.selectedServer ?? state.activeServer
state.activeServer = state.initialServer
var input = state.selectedServer ?? state.activeServer
var isCustom = false
state.selectedServer = nil
if input == L10n.ServerSetup.custom {
input = state.customServer
isCustom = true
}
if let serverConfig = UserPreferencesStorage.ServerConfig.config(
for: input,
isCustom: isCustom,
streamingCallTimeoutInMillis: streamingCallTimeoutInMillis
) {
do {
try userStoredPreferences.setServer(serverConfig)
} catch UserPreferencesStorage.UserPreferencesStorageError.serverConfig {
return .send(.switchFailed(ZcashError.unknown(UserPreferencesStorage.UserPreferencesStorageError.serverConfig)))
} catch {
return .send(.switchFailed(ZcashError.unknown(error)))
}
}
return .none
}
}
}
}
// MARK: Alerts
extension AlertState where Action == ServerSetup.Action {
public static func endpoindSwitchFailed(_ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.ServerSetup.Alert.Failed.title)
} actions: {
ButtonState(action: .alert(.dismiss)) {
TextState(L10n.General.ok)
}
} message: {
TextState(L10n.ServerSetup.Alert.Failed.message(error.detailedMessage))
}
}
}