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

[#1028] Runtime switch of lightwalletd servers (#1044)

- changelog update

[#1028] Runtime switch of lightwalletd servers (#1044)

- Unfortunately the compiler has a bug so Circular reference error is not possible to solve, Apple fixed reported issue from October 2023 last week so we should expect fix in Xcode 15.3, beta is released but still no fix. Until that moment I moved placeholders to the view and will move it back to the stores once the issue is resolved
  • Loading branch information
LukasKorba committed Feb 12, 2024
1 parent 79648f5 commit ab0fae3
Show file tree
Hide file tree
Showing 16 changed files with 502 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ directly impact users rather than highlighting other crucial architectural updat

### Added
- Pending values (changes) at the Balances tab.
- Choose a Server feature: available at settings, pre-defined servers + custom server setup.

### Fixed
- Failed transactions are no longer at the top of the transaction history but mixed with the transactions around the time it failed.
Expand Down
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 ab0fae3

Please sign in to comment.