Skip to content

Commit

Permalink
[#1028] Runtime switch of lightwalletd servers
Browse files Browse the repository at this point in the history
- prototype of the solution implemented

[#1028] Runtime switch of lightwalletd servers

- error handling done
- localized all new texts
- custom server resolved with all possible parsing states
- persistency of selected server done
  • Loading branch information
LukasKorba committed Feb 10, 2024
1 parent 79648f5 commit 72eae15
Show file tree
Hide file tree
Showing 15 changed files with 487 additions and 19 deletions.
21 changes: 19 additions & 2 deletions modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ let package = Package(
.library(name: "SecItem", targets: ["SecItem"]),
.library(name: "SecurityWarning", targets: ["SecurityWarning"]),
.library(name: "SendFlow", targets: ["SendFlow"]),
.library(name: "ServerSetup", targets: ["ServerSetup"]),
.library(name: "Settings", targets: ["Settings"]),
.library(name: "SupportDataGenerator", targets: ["SupportDataGenerator"]),
.library(name: "SyncProgress", targets: ["SyncProgress"]),
Expand All @@ -62,11 +63,12 @@ let package = Package(
.library(name: "ZcashSDKEnvironment", targets: ["ZcashSDKEnvironment"])
],
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.4.2"),
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.7.0"),
.package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.1.0"),
.package(url: "https://github.com/pointfreeco/swift-url-routing", from: "0.6.0"),
.package(url: "https://github.com/zcash-hackworks/MnemonicSwift", from: "2.2.4"),
.package(url: "https://github.com/zcash/ZcashLightClientKit", from: "2.0.9"),
// .package(url: "https://github.com/zcash/ZcashLightClientKit", from: "2.0.9"),
.package(url: "https://github.com/LukasKorba/ZcashLightClientKit", branch: "1153-Allow-runtime-switch-of-lightwalletd-servers"),
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.17.0")
],
targets: [
Expand Down Expand Up @@ -468,6 +470,19 @@ let package = Package(
],
path: "Sources/Features/SendFlow"
),
.target(
name: "ServerSetup",
dependencies: [
"Generated",
"SDKSynchronizer",
"UIComponents",
"UserDefaults",
"ZcashSDKEnvironment",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ZcashLightClientKit", package: "ZcashLightClientKit")
],
path: "Sources/Features/ServerSetup"
),
.target(
name: "Settings",
dependencies: [
Expand All @@ -480,6 +495,7 @@ let package = Package(
"RecoveryPhraseDisplay",
"RestoreWalletStorage",
"SDKSynchronizer",
"ServerSetup",
"SupportDataGenerator",
"UIComponents",
"WalletStorage",
Expand Down Expand Up @@ -628,6 +644,7 @@ let package = Package(
.target(
name: "ZcashSDKEnvironment",
dependencies: [
"UserDefaults",
.product(name: "ZcashLightClientKit", package: "ZcashLightClientKit"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ public struct SDKSynchronizerClient {
public let shieldFunds: (UnifiedSpendingKey, Memo, Zatoshi) async throws -> TransactionState

public var wipe: () -> AnyPublisher<Void, Error>?

public var switchToEndpoint: (LightWalletEndpoint) async throws -> Void
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ extension SDKSynchronizerClient {
latestBlockHeight: try await SDKSynchronizerClient.latestBlockHeight(synchronizer: synchronizer)
)
},
wipe: { synchronizer.wipe() }
wipe: { synchronizer.wipe() },
switchToEndpoint: { endpoint in
try await synchronizer.switchTo(endpoint: endpoint)
}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ extension SDKSynchronizerClient: TestDependencyKey {
getSaplingAddress: XCTUnimplemented("\(Self.self).getSaplingAddress", placeholder: nil),
sendTransaction: XCTUnimplemented("\(Self.self).sendTransaction", placeholder: .placeholder()),
shieldFunds: XCTUnimplemented("\(Self.self).shieldFunds", placeholder: .placeholder()),
wipe: XCTUnimplemented("\(Self.self).wipe")
wipe: XCTUnimplemented("\(Self.self).wipe"),
switchToEndpoint: XCTUnimplemented("\(Self.self).switchToEndpoint")
)
}

Expand All @@ -50,7 +51,8 @@ extension SDKSynchronizerClient {
getSaplingAddress: { _ in return nil },
sendTransaction: { _, _, _, _ in return .placeholder() },
shieldFunds: { _, _, _ in return .placeholder() },
wipe: { Empty<Void, Error>().eraseToAnyPublisher() }
wipe: { Empty<Void, Error>().eraseToAnyPublisher() },
switchToEndpoint: { _ in }
)

public static let mock = Self.mocked()
Expand Down Expand Up @@ -169,7 +171,8 @@ extension SDKSynchronizerClient {
zecAmount: Zatoshi(10)
)
},
wipe: @escaping () -> AnyPublisher<Void, Error>? = { Fail(error: "Error").eraseToAnyPublisher() }
wipe: @escaping () -> AnyPublisher<Void, Error>? = { Fail(error: "Error").eraseToAnyPublisher() },
switchToEndpoint: @escaping (LightWalletEndpoint) async throws -> Void = { _ in }
) -> SDKSynchronizerClient {
SDKSynchronizerClient(
stateStream: stateStream,
Expand All @@ -187,7 +190,8 @@ extension SDKSynchronizerClient {
getSaplingAddress: getSaplingAddress,
sendTransaction: sendTransaction,
shieldFunds: shieldFunds,
wipe: wipe
wipe: wipe,
switchToEndpoint: switchToEndpoint
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import ComposableArchitecture
import ZcashLightClientKit

import UserDefaults

extension DependencyValues {
public var zcashSDKEnvironment: ZcashSDKEnvironment {
get { self[ZcashSDKEnvironment.self] }
Expand All @@ -16,6 +18,85 @@ extension DependencyValues {
}

extension ZcashSDKEnvironment {
public enum Servers: String, CaseIterable, Equatable {
public enum Constants {
public static let udServerKey = "zashi_udServerKey"
public static let udCustomServerKey = "zashi_udCustomServerKey"
}

case mainnet
case naNW
case saNW
case euNW
case aiNW
case custom

public func server() -> String {
switch self {
case .mainnet: return "mainnet.lightwalletd.com:9067"
case .naNW: return "na.lightwalletd.com:443"
case .saNW: return "sa.lightwalletd.com:443"
case .euNW: return "eu.lightwalletd.com:443"
case .aiNW: return "ai.lightwalletd.com:443"
case .custom: return "custom"
}
}

public func lightWalletEndpoint(_ userDefaults: UserDefaultsClient) -> LightWalletEndpoint? {
switch self {
case .mainnet:
return LightWalletEndpoint(
address: "mainnet.lightwalletd.com",
port: 9067,
secure: true,
streamingCallTimeoutInMillis: ZcashSDKConstants.streamingCallTimeoutInMillis
)
case .naNW, .saNW, .euNW, .aiNW:
return LightWalletEndpoint(
address: String(self.server().dropLast(4)),
port: 443,
secure: true,
streamingCallTimeoutInMillis: ZcashSDKConstants.streamingCallTimeoutInMillis
)
case .custom:
let udKey = ZcashSDKEnvironment.Servers.Constants.udCustomServerKey
if let storedCustomServer = userDefaults.objectForKey(udKey) as? String{
// remove http:// or https:// from the input if present
var input = storedCustomServer

if input.contains("https://") {
input = String(input.dropFirst(8))
} else if input.contains("http://") {
input = String(input.dropFirst(7))
}

let split = input.split(separator: ":")

if let portString = split.last, let port = Int(portString) {
var host = ""

if split.count == 2, let first = split.first {
host = String(first)
} else if split.count == 3, let first = split.first {
let second = split[1]

host = "\(String(first))\(String(second))"
}

return LightWalletEndpoint(
address: host,
port: port,
secure: true,
streamingCallTimeoutInMillis: ZcashSDKConstants.streamingCallTimeoutInMillis
)
}
}

return nil
}
}
}

public enum ZcashSDKConstants {
static let endpointMainnetAddress = "mainnet.lightwalletd.com"
static let endpointTestnetAddress = "lightwalletd.testnet.electriccoin.co"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,26 @@ extension ZcashSDKEnvironment: DependencyKey {
public static let liveValue = Self(
latestCheckpoint: { network in BlockHeight.ofLatestCheckpoint(network: network) },
endpoint: { network in
LightWalletEndpoint(
// In case of mainnet network we may have stored server as a user action in advanced settings
if network.networkType == .mainnet {
@Dependency(\.userDefaults) var userDefaults

let udKey = ZcashSDKEnvironment.Servers.Constants.udServerKey

if let storedServerRaw = userDefaults.objectForKey(udKey) as? String,
let storedServer = ZcashSDKEnvironment.Servers(rawValue: storedServerRaw) {
if let endpoint = storedServer.lightWalletEndpoint(userDefaults) {
// Some endpoint is set by a user so we initialize the SDK with this one
return endpoint
} else {
// Endpoint failed, fallback to hardcoded mainnet
userDefaults.setValue(ZcashSDKEnvironment.Servers.mainnet.rawValue, udKey)
}
}
}

// Hardcoded endpoint
return LightWalletEndpoint(
address: Self.endpoint(for: network),
port: ZcashSDKConstants.endpointPort,
secure: true,
Expand Down
145 changes: 145 additions & 0 deletions modules/Sources/Features/ServerSetup/ServerSetupStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//
// ServerSetup.swift
// secant-testnet
//
// Created by Lukáš Korba on 2024-02-07.
//

import Foundation
import ComposableArchitecture
import ZcashLightClientKit

import Generated
import SDKSynchronizer
import ZcashSDKEnvironment

@Reducer
public struct ServerSetup {
let udKey = ZcashSDKEnvironment.Servers.Constants.udServerKey
let udCustomServerKey = ZcashSDKEnvironment.Servers.Constants.udCustomServerKey

@ObservableState
public struct State: Equatable {
@Presents var alert: AlertState<Action>?
var isUpdatingServer = false
var initialServer: ZcashSDKEnvironment.Servers = .mainnet
var server: ZcashSDKEnvironment.Servers = .mainnet
var customServer: String

public init(
isUpdatingServer: Bool = false,
server: ZcashSDKEnvironment.Servers = .mainnet,
customServer: String = ""
) {
self.isUpdatingServer = isUpdatingServer
self.server = server
self.customServer = customServer
}
}

public enum Action: Equatable, BindableAction {
case alert(PresentationAction<Action>)
case binding(BindingAction<State>)
case onAppear
case setServerTapped
case someServerTapped(ZcashSDKEnvironment.Servers)
case switchFailed(ZcashError)
case switchSucceeded
}

public init() {}

@Dependency(\.mainQueue) var mainQueue
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.userDefaults) var userDefaults

public var body: some ReducerOf<Self> {
BindingReducer()

Reduce { state, action in
switch action {
case .onAppear:
guard let storedServerRaw = userDefaults.objectForKey(udKey) as? String, let storedServer = ZcashSDKEnvironment.Servers(rawValue: storedServerRaw) else {
return .none
}
if let storedCustomServerRaw = userDefaults.objectForKey(udCustomServerKey) as? String {
state.customServer = storedCustomServerRaw
}
state.server = storedServer
state.initialServer = storedServer
return .none

case .alert(.dismiss):
state.alert = nil
return .none

case .alert:
return .none

case .binding:
return .none

case .setServerTapped:
guard state.initialServer != state.server || state.server == .custom else {
return .none
}

state.isUpdatingServer = true

// custom server needs to be stored first
if state.server == .custom {
userDefaults.setValue(state.customServer, udCustomServerKey)
}

return .run { [server = state.server] send in
do {
guard let lightWalletEndpoint = server.lightWalletEndpoint(userDefaults) else {
throw ZcashError.synchronizerServerSwitch
}
try await sdkSynchronizer.switchToEndpoint(lightWalletEndpoint)
try await mainQueue.sleep(for: .seconds(1))
await send(.switchSucceeded)
} catch {
await send(.switchFailed(error.toZcashError()))
}
}

case .someServerTapped(let newChange):
state.server = newChange
return .none

case .switchFailed(let error):
state.isUpdatingServer = false
userDefaults.remove(udCustomServerKey)
state.alert = AlertState.endpoindSwitchFailed(error)
return .none

case .switchSucceeded:
userDefaults.setValue(state.server.rawValue, udKey)
state.isUpdatingServer = false
state.initialServer = state.server
if state.server != .custom {
userDefaults.remove(udCustomServerKey)
state.customServer = ""
}
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.message, error.code.rawValue))
}
}
}

0 comments on commit 72eae15

Please sign in to comment.