react-native-zcash 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/android/build.gradle +3 -3
  3. package/android/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2650000.json +8 -0
  4. package/android/src/main/java/app/edge/rnzcash/RNZcashModule.kt +15 -25
  5. package/ios/RNZcash.m +3 -5
  6. package/ios/RNZcash.swift +42 -23
  7. package/ios/ZCashLightClientKit/Block/Actions/Action.swift +1 -0
  8. package/ios/ZCashLightClientKit/Block/Actions/EnhanceAction.swift +1 -1
  9. package/ios/ZCashLightClientKit/Block/Actions/ProcessSuggestedScanRangesAction.swift +1 -1
  10. package/ios/ZCashLightClientKit/Block/Actions/ScanAction.swift +1 -1
  11. package/ios/ZCashLightClientKit/Block/Actions/TxResubmissionAction.swift +75 -0
  12. package/ios/ZCashLightClientKit/Block/Actions/UpdateChainTipAction.swift +1 -1
  13. package/ios/ZCashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift +3 -1
  14. package/ios/ZCashLightClientKit/Block/CompactBlockProcessor.swift +14 -0
  15. package/ios/ZCashLightClientKit/Block/Download/BlockDownloaderService.swift +2 -2
  16. package/ios/ZCashLightClientKit/Block/Enhance/BlockEnhancer.swift +54 -49
  17. package/ios/ZCashLightClientKit/Checkpoint/BundleCheckpointSource.swift +1 -6
  18. package/ios/ZCashLightClientKit/Checkpoint/CheckpointSourceFactory.swift +1 -1
  19. package/ios/ZCashLightClientKit/ClosureSynchronizer.swift +3 -1
  20. package/ios/ZCashLightClientKit/CombineSynchronizer.swift +6 -2
  21. package/ios/ZCashLightClientKit/Constants/ZcashSDK.swift +15 -0
  22. package/ios/ZCashLightClientKit/DAO/TransactionDao.swift +20 -4
  23. package/ios/ZCashLightClientKit/Entity/TransactionEntity.swift +27 -24
  24. package/ios/ZCashLightClientKit/Error/Sourcery/generateErrorCode.sh +2 -2
  25. package/ios/ZCashLightClientKit/Error/ZcashError.swift +32 -1
  26. package/ios/ZCashLightClientKit/Error/ZcashErrorCode.swift +11 -1
  27. package/ios/ZCashLightClientKit/Error/ZcashErrorCodeDefinition.swift +18 -0
  28. package/ios/ZCashLightClientKit/Initializer.swift +22 -14
  29. package/ios/ZCashLightClientKit/Metrics/SDKMetrics.swift +0 -1
  30. package/ios/ZCashLightClientKit/Model/FiatCurrencyResult.swift +25 -0
  31. package/ios/ZCashLightClientKit/Model/Proposal.swift +1 -1
  32. package/ios/ZCashLightClientKit/Model/TransactionDataRequest.swift +26 -0
  33. package/ios/ZCashLightClientKit/Model/WalletTypes.swift +39 -1
  34. package/ios/ZCashLightClientKit/Model/Zatoshi.swift +1 -1
  35. package/ios/ZCashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift +39 -2
  36. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/service.proto +5 -4
  37. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/service.grpc.swift +819 -3
  38. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/service.pb.swift +2 -2
  39. package/ios/ZCashLightClientKit/Modules/Service/LightWalletService.swift +3 -1
  40. package/ios/ZCashLightClientKit/Providers/ResourceProvider.swift +10 -0
  41. package/ios/ZCashLightClientKit/Repository/TransactionRepository.swift +4 -0
  42. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2475000.json +8 -0
  43. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2477500.json +8 -0
  44. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2482500.json +8 -0
  45. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2485000.json +8 -0
  46. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2487500.json +8 -0
  47. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2492500.json +8 -0
  48. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2495000.json +8 -0
  49. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2497500.json +8 -0
  50. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2502500.json +8 -0
  51. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2505000.json +8 -0
  52. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2507500.json +8 -0
  53. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2512500.json +8 -0
  54. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2515000.json +8 -0
  55. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2517500.json +8 -0
  56. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2522500.json +8 -0
  57. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2525000.json +8 -0
  58. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2527500.json +8 -0
  59. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2532500.json +8 -0
  60. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2535000.json +8 -0
  61. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2537500.json +8 -0
  62. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2542500.json +8 -0
  63. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2545000.json +8 -0
  64. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2547500.json +8 -0
  65. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2552500.json +8 -0
  66. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2555000.json +8 -0
  67. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2557500.json +8 -0
  68. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2562500.json +8 -0
  69. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2565000.json +8 -0
  70. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2567500.json +8 -0
  71. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2572500.json +8 -0
  72. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2575000.json +8 -0
  73. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2577500.json +8 -0
  74. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2582500.json +8 -0
  75. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2585000.json +8 -0
  76. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2587500.json +8 -0
  77. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2592500.json +8 -0
  78. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2595000.json +8 -0
  79. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2597500.json +8 -0
  80. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2602500.json +8 -0
  81. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2605000.json +8 -0
  82. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2607500.json +8 -0
  83. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2612500.json +8 -0
  84. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2615000.json +8 -0
  85. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2617500.json +8 -0
  86. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2622500.json +8 -0
  87. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2625000.json +8 -0
  88. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2627500.json +8 -0
  89. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2632500.json +8 -0
  90. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2635000.json +8 -0
  91. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2637500.json +8 -0
  92. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2642500.json +8 -0
  93. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2645000.json +8 -0
  94. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2647500.json +8 -0
  95. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2650000.json +8 -0
  96. package/ios/ZCashLightClientKit/Rust/ZcashKeyDerivationBackend.swift +4 -24
  97. package/ios/ZCashLightClientKit/Rust/ZcashKeyDerivationBackendWelding.swift +0 -15
  98. package/ios/ZCashLightClientKit/Rust/ZcashRustBackend.swift +133 -32
  99. package/ios/ZCashLightClientKit/Rust/ZcashRustBackendWelding.swift +18 -1
  100. package/ios/ZCashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift +4 -0
  101. package/ios/ZCashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift +5 -1
  102. package/ios/ZCashLightClientKit/Synchronizer/Dependencies.swift +53 -3
  103. package/ios/ZCashLightClientKit/Synchronizer/SDKSynchronizer.swift +236 -7
  104. package/ios/ZCashLightClientKit/Synchronizer.swift +28 -0
  105. package/ios/ZCashLightClientKit/Tool/DerivationTool.swift +29 -5
  106. package/ios/ZCashLightClientKit/Tor/TorClient.swift +57 -0
  107. package/ios/ZCashLightClientKit/Utils/LoggingProxy.swift +4 -0
  108. package/ios/ZCashLightClientKit/Utils/OSLogger.swift +4 -0
  109. package/ios/libzcashlc.xcframework/ios-arm64/libzcashlc.a +0 -0
  110. package/ios/libzcashlc.xcframework/ios-arm64_x86_64-simulator/libzcashlc.a +0 -0
  111. package/ios/zcashlc.h +287 -42
  112. package/lib/rnzcash.rn.js +8 -8
  113. package/lib/rnzcash.rn.js.map +1 -1
  114. package/lib/src/react-native.d.ts +3 -3
  115. package/lib/src/types.d.ts +4 -2
  116. package/package.json +1 -1
  117. package/src/react-native.ts +13 -18
  118. package/src/types.ts +5 -2
@@ -14,8 +14,7 @@ enum Dependencies {
14
14
  alias: ZcashSynchronizerAlias,
15
15
  networkType: NetworkType,
16
16
  endpoint: LightWalletEndpoint,
17
- loggingPolicy: Initializer.LoggingPolicy = .default(.debug),
18
- enableBackendTracing: Bool = false
17
+ loggingPolicy: Initializer.LoggingPolicy = .default(.debug)
19
18
  ) {
20
19
  container.register(type: CheckpointSource.self, isSingleton: true) { _ in
21
20
  CheckpointSourceFactory.fromBundle(for: networkType)
@@ -35,6 +34,36 @@ enum Dependencies {
35
34
  return logger
36
35
  }
37
36
 
37
+ let rustLogging: RustLogging
38
+ switch loggingPolicy {
39
+ case .default(let logLevel):
40
+ switch logLevel {
41
+ case .debug:
42
+ rustLogging = RustLogging.debug
43
+ case .info, .event:
44
+ rustLogging = RustLogging.info
45
+ case .warning:
46
+ rustLogging = RustLogging.warn
47
+ case .error:
48
+ rustLogging = RustLogging.error
49
+ }
50
+ case .custom(let logger):
51
+ switch logger.maxLogLevel() {
52
+ case .debug:
53
+ rustLogging = RustLogging.debug
54
+ case .info, .event:
55
+ rustLogging = RustLogging.info
56
+ case .warning:
57
+ rustLogging = RustLogging.warn
58
+ case .error:
59
+ rustLogging = RustLogging.error
60
+ case .none:
61
+ rustLogging = RustLogging.off
62
+ }
63
+ case .noLogging:
64
+ rustLogging = RustLogging.off
65
+ }
66
+
38
67
  container.register(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in
39
68
  ZcashRustBackend(
40
69
  dbData: urls.dataDbURL,
@@ -42,7 +71,7 @@ enum Dependencies {
42
71
  spendParamsPath: urls.spendParamsURL,
43
72
  outputParamsPath: urls.outputParamsURL,
44
73
  networkType: networkType,
45
- enableTracing: enableBackendTracing
74
+ logLevel: rustLogging
46
75
  )
47
76
  }
48
77
 
@@ -96,6 +125,25 @@ enum Dependencies {
96
125
  container.register(type: ZcashFileManager.self, isSingleton: true) { _ in
97
126
  FileManager.default
98
127
  }
128
+
129
+ container.register(type: TransactionEncoder.self, isSingleton: true) { di in
130
+ let service = di.resolve(LightWalletService.self)
131
+ let logger = di.resolve(Logger.self)
132
+ let transactionRepository = di.resolve(TransactionRepository.self)
133
+ let rustBackend = di.resolve(ZcashRustBackendWelding.self)
134
+
135
+ return WalletTransactionEncoder(
136
+ rustBackend: rustBackend,
137
+ dataDb: urls.dataDbURL,
138
+ fsBlockDbRoot: urls.fsBlockDbRoot,
139
+ service: service,
140
+ repository: transactionRepository,
141
+ outputParams: urls.outputParamsURL,
142
+ spendParams: urls.spendParamsURL,
143
+ networkType: networkType,
144
+ logger: logger
145
+ )
146
+ }
99
147
  }
100
148
 
101
149
  static func setupCompactBlockProcessor(
@@ -145,6 +193,7 @@ enum Dependencies {
145
193
  let rustBackend = di.resolve(ZcashRustBackendWelding.self)
146
194
  let transactionRepository = di.resolve(TransactionRepository.self)
147
195
  let metrics = di.resolve(SDKMetrics.self)
196
+ let service = di.resolve(LightWalletService.self)
148
197
  let logger = di.resolve(Logger.self)
149
198
 
150
199
  return BlockEnhancerImpl(
@@ -152,6 +201,7 @@ enum Dependencies {
152
201
  rustBackend: rustBackend,
153
202
  transactionRepository: transactionRepository,
154
203
  metrics: metrics,
204
+ service: service,
155
205
  logger: logger
156
206
  )
157
207
  }
@@ -12,7 +12,6 @@ import Combine
12
12
  /// Synchronizer implementation for UIKit and iOS 13+
13
13
  // swiftlint:disable type_body_length
14
14
  public class SDKSynchronizer: Synchronizer {
15
-
16
15
  public var alias: ZcashSynchronizerAlias { initializer.alias }
17
16
 
18
17
  private lazy var streamsUpdateQueue = { DispatchQueue(label: "streamsUpdateQueue_\(initializer.alias.description)") }()
@@ -23,8 +22,12 @@ public class SDKSynchronizer: Synchronizer {
23
22
  private let eventSubject = PassthroughSubject<SynchronizerEvent, Never>()
24
23
  public var eventStream: AnyPublisher<SynchronizerEvent, Never> { eventSubject.eraseToAnyPublisher() }
25
24
 
25
+ private let exchangeRateUSDSubject = CurrentValueSubject<FiatCurrencyResult?, Never>(nil)
26
+ public var exchangeRateUSDStream: AnyPublisher<FiatCurrencyResult?, Never> { exchangeRateUSDSubject.eraseToAnyPublisher() }
27
+
26
28
  let metrics: SDKMetrics
27
29
  public let logger: Logger
30
+ var tor: TorClient?
28
31
 
29
32
  // Don't read this variable directly. Use `status` instead. And don't update this variable directly use `updateStatus()` methods instead.
30
33
  private var underlyingStatus: GenericActor<InternalSyncStatus>
@@ -83,12 +86,14 @@ public class SDKSynchronizer: Synchronizer {
83
86
  self.syncSession = SyncSession(.nullID)
84
87
  self.syncSessionTicker = syncSessionTicker
85
88
  self.latestBlocksDataProvider = initializer.container.resolve(LatestBlocksDataProvider.self)
86
-
89
+
87
90
  initializer.lightWalletService.connectionStateChange = { [weak self] oldState, newState in
88
91
  self?.connectivityStateChanged(oldState: oldState, newState: newState)
89
92
  }
90
93
 
91
- Task(priority: .high) { [weak self] in await self?.subscribeToProcessorEvents(blockProcessor) }
94
+ Task(priority: .high) { [weak self] in
95
+ await self?.subscribeToProcessorEvents(blockProcessor)
96
+ }
92
97
  }
93
98
 
94
99
  deinit {
@@ -300,9 +305,9 @@ public class SDKSynchronizer: Synchronizer {
300
305
  do {
301
306
  try throwIfUnprepared()
302
307
  return try await transactionEncoder.proposeFulfillingPaymentFromURI(
303
- uri,
304
- accountIndex: accountIndex
305
- )
308
+ uri,
309
+ accountIndex: accountIndex
310
+ )
306
311
  } catch ZcashError.rustCreateToAddress(let error) {
307
312
  throw ZcashError.rustProposeTransferFromURI(error)
308
313
  } catch {
@@ -480,8 +485,12 @@ public class SDKSynchronizer: Synchronizer {
480
485
  PagedTransactionRepositoryBuilder.build(initializer: initializer, kind: .all)
481
486
  }
482
487
 
488
+ public func getMemos(for rawID: Data) async throws -> [Memo] {
489
+ return try await transactionRepository.findMemos(for: rawID)
490
+ }
491
+
483
492
  public func getMemos(for transaction: ZcashTransaction.Overview) async throws -> [Memo] {
484
- return try await transactionRepository.findMemos(for: transaction)
493
+ return try await transactionRepository.findMemos(for: transaction.rawID)
485
494
  }
486
495
 
487
496
  public func getRecipients(for transaction: ZcashTransaction.Overview) async -> [TransactionRecipient] {
@@ -505,6 +514,41 @@ public class SDKSynchronizer: Synchronizer {
505
514
  try await initializer.rustBackend.getWalletSummary()?.accountBalances[UInt32(accountIndex)]
506
515
  }
507
516
 
517
+ /// Fetches the latest ZEC-USD exchange rate.
518
+ public func refreshExchangeRateUSD() {
519
+ // ignore refresh request when one is already in flight
520
+ if let latestState = tor?.cachedFiatCurrencyResult?.state, latestState == .fetching {
521
+ return
522
+ }
523
+
524
+ // broadcast cached value but update the state
525
+ if let cachedFiatCurrencyResult = tor?.cachedFiatCurrencyResult {
526
+ var fetchingState = cachedFiatCurrencyResult
527
+ fetchingState.state = .fetching
528
+ tor?.cachedFiatCurrencyResult = fetchingState
529
+
530
+ exchangeRateUSDSubject.send(fetchingState)
531
+ }
532
+
533
+ Task {
534
+ do {
535
+ if tor == nil {
536
+ logger.info("Bootstrapping Tor client for fetching exchange rates")
537
+ tor = try await TorClient(torDir: initializer.torDirURL)
538
+ }
539
+ // broadcast new value in case of success
540
+ exchangeRateUSDSubject.send(try await tor?.getExchangeRateUSD())
541
+ } catch {
542
+ // broadcast cached value but update the state
543
+ var errorState = tor?.cachedFiatCurrencyResult
544
+ errorState?.state = .error
545
+ tor?.cachedFiatCurrencyResult = errorState
546
+
547
+ exchangeRateUSDSubject.send(errorState)
548
+ }
549
+ }
550
+ }
551
+
508
552
  public func getUnifiedAddress(accountIndex: Int) async throws -> UnifiedAddress {
509
553
  try await blockProcessor.getUnifiedAddress(accountIndex: accountIndex)
510
554
  }
@@ -608,6 +652,191 @@ public class SDKSynchronizer: Synchronizer {
608
652
  try await initializer.rustBackend.isSeedRelevantToAnyDerivedAccount(seed: seed)
609
653
  }
610
654
 
655
+ /// Takes the list of endpoints and runs it through a series of checks to evaluate its performance.
656
+ /// - Parameters:
657
+ /// - endpoints: Array of endpoints to evaluate.
658
+ /// - latencyThresholdMillis: The mean latency of `getInfo` and `getTheLatestHeight` calls must be below this threshold. The default is 300 ms.
659
+ /// - fetchThresholdSeconds: The time to download `nBlocksToFetch` blocks from the stream must be below this threshold. The default is 60 seconds.
660
+ /// - nBlocksToFetch: The number of blocks expected to be downloaded from the stream, with the time compared to `fetchThresholdSeconds`. The default is 100.
661
+ /// - kServers: The expected number of endpoints in the output. The default is 3.
662
+ /// - network: Mainnet or testnet. The default is mainnet.
663
+ public func evaluateBestOf(
664
+ endpoints: [LightWalletEndpoint],
665
+ latencyThresholdMillis: Double = 300.0,
666
+ fetchThresholdSeconds: Double = 60.0,
667
+ nBlocksToFetch: UInt64 = 100,
668
+ kServers: Int = 3,
669
+ network: NetworkType = .mainnet
670
+ ) async -> [LightWalletEndpoint] {
671
+ struct Service {
672
+ let originalEndpoint: LightWalletEndpoint
673
+ let service: LightWalletGRPCService
674
+ let url: String
675
+ }
676
+
677
+ struct CheckResult {
678
+ let id: String
679
+ let info: LightWalletdInfo?
680
+ let getInfoTime: TimeInterval
681
+ let latestBlockHeight: BlockHeight?
682
+ let latestBlockHeightTime: TimeInterval
683
+ let mean: TimeInterval
684
+ let service: Service
685
+ var blockTime: TimeInterval
686
+ }
687
+
688
+ // Initialize services for the endpoints
689
+ let services = endpoints.map {
690
+ Service(
691
+ originalEndpoint: $0,
692
+ service: LightWalletGRPCService(
693
+ host: $0.host,
694
+ port: $0.port,
695
+ secure: $0.secure,
696
+ singleCallTimeout: 5000,
697
+ streamingCallTimeout: Int64(fetchThresholdSeconds) * 1000
698
+ ),
699
+ url: "\($0.host):\($0.port)"
700
+ )
701
+ }
702
+
703
+ // Parallel part
704
+ var checkResults: [String: CheckResult] = [:]
705
+
706
+ await withTaskGroup(of: CheckResult.self) { group in
707
+ for service in services {
708
+ group.addTask {
709
+ let startTime = Date().timeIntervalSince1970
710
+ let info = try? await service.service.getInfo()
711
+ let markTime = Date().timeIntervalSince1970
712
+ let latestBlockHeight = try? await service.service.latestBlockHeight()
713
+ let endTime = Date().timeIntervalSince1970
714
+
715
+ let getInfoTime = markTime - startTime
716
+ let latestBlockHeightTime = endTime - markTime
717
+ let mean = (getInfoTime + latestBlockHeightTime) / 2
718
+
719
+ return CheckResult(
720
+ id: service.url,
721
+ info: info,
722
+ getInfoTime: getInfoTime,
723
+ latestBlockHeight: latestBlockHeight,
724
+ latestBlockHeightTime: latestBlockHeightTime,
725
+ mean: mean,
726
+ service: service,
727
+ blockTime: 0
728
+ )
729
+ }
730
+ }
731
+
732
+ var tmpResults: [String: CheckResult] = [:]
733
+
734
+ for await result in group {
735
+ // rule out results where calls failed
736
+ guard let info = result.info, result.latestBlockHeight != nil else {
737
+ continue
738
+ }
739
+
740
+ // rule out if mismatch of networks
741
+ guard (info.chainName == "main" && network == .mainnet)
742
+ || (info.chainName == "test" && network == .testnet) else {
743
+ continue
744
+ }
745
+
746
+ // rule out mismatch of consensus branch IDs
747
+ guard let localBranchID = await blockProcessor.consensusBranchIdFor(Int32(info.blockHeight)) else {
748
+ continue
749
+ }
750
+
751
+ guard let remoteBranchID = ConsensusBranchID.fromString(info.consensusBranchID) else {
752
+ continue
753
+ }
754
+
755
+ guard remoteBranchID == localBranchID else {
756
+ continue
757
+ }
758
+
759
+ // Rule out servers that are syncing, stuck, or probably on the wrong fork.
760
+ // To avoid falsely ruling out all servers this can only be a very loose check
761
+ // (i.e. `ZcashSDK.syncedThresholdBlocks` should not be too small),
762
+ // because `info.estimatedHeight` may be quite inaccurate.
763
+ guard info.blockHeight + ZcashSDK.syncedThresholdBlocks >= info.estimatedHeight else {
764
+ continue
765
+ }
766
+
767
+ tmpResults[result.id] = result
768
+ }
769
+
770
+ // rule out all means above latencyThreshold
771
+ let underThreshold = tmpResults.compactMap {
772
+ $0.value.mean < (latencyThresholdMillis / 1000.0) ? $0 : nil
773
+ }
774
+
775
+ // sort the server responses by mean
776
+ let sortedUnderThreshold = underThreshold.sorted {
777
+ $0.value.mean < $1.value.mean
778
+ }
779
+
780
+ // retain only k servers
781
+ let sortedUnderThresholdKOnly = sortedUnderThreshold.prefix(3)
782
+
783
+ sortedUnderThresholdKOnly.forEach {
784
+ checkResults[$0.key] = $0.value
785
+ }
786
+ }
787
+
788
+ // Sequential part
789
+ var blockResults: [String: CheckResult] = [:]
790
+
791
+ for serviceDict in checkResults {
792
+ guard let info = serviceDict.value.info else {
793
+ continue
794
+ }
795
+
796
+ let service = serviceDict.value.service
797
+
798
+ guard info.blockHeight >= nBlocksToFetch else {
799
+ continue
800
+ }
801
+
802
+ let stream = service.service.blockStream(startHeight: BlockHeight(info.blockHeight - nBlocksToFetch), endHeight: BlockHeight(info.blockHeight))
803
+
804
+ do {
805
+ let startTime = Date().timeIntervalSince1970
806
+ var endTime = startTime
807
+ for try await _ in stream {
808
+ endTime = Date().timeIntervalSince1970
809
+ if endTime - startTime >= fetchThresholdSeconds {
810
+ break
811
+ }
812
+ }
813
+
814
+ let blockTime = endTime - startTime
815
+
816
+ // rule out servers that can't fetch `nBlocksToFetch` blocks under fetchThresholdSeconds
817
+ if blockTime < fetchThresholdSeconds {
818
+ var value = serviceDict.value
819
+ value.blockTime = blockTime
820
+
821
+ blockResults[serviceDict.key] = value
822
+ }
823
+ } catch {
824
+ continue
825
+ }
826
+ }
827
+
828
+ // return what's left
829
+ let sortedServers = blockResults.sorted {
830
+ $0.value.blockTime < $1.value.blockTime
831
+ }
832
+
833
+ let finalResult = sortedServers.map {
834
+ $0.value.service.originalEndpoint
835
+ }
836
+
837
+ return finalResult
838
+ }
839
+
611
840
  // MARK: Server switch
612
841
 
613
842
  public func switchTo(endpoint: LightWalletEndpoint) async throws {
@@ -101,6 +101,9 @@ public protocol Synchronizer: AnyObject {
101
101
  /// This stream is backed by `PassthroughSubject`. Check `SynchronizerEvent` to see which events may be emitted.
102
102
  var eventStream: AnyPublisher<SynchronizerEvent, Never> { get }
103
103
 
104
+ /// This stream emits the latest known USD/ZEC exchange rate, paired with the time it was queried. See `FiatCurrencyResult`.
105
+ var exchangeRateUSDStream: AnyPublisher<FiatCurrencyResult?, Never> { get }
106
+
104
107
  /// Initialize the wallet. The ZIP-32 seed bytes can optionally be passed to perform
105
108
  /// database migrations. most of the times the seed won't be needed. If they do and are
106
109
  /// not provided this will fail with `InitializationResult.seedRequired`. It could
@@ -263,6 +266,11 @@ public protocol Synchronizer: AnyObject {
263
266
  /// - Parameter kind: Transaction Kind expected from this PaginatedTransactionRepository
264
267
  func paginatedTransactions(of kind: TransactionKind) -> PaginatedTransactionRepository
265
268
 
269
+ /// Get all memos for `transaction.rawID`.
270
+ ///
271
+ // sourcery: mockedName="getMemosForRawID"
272
+ func getMemos(for rawID: Data) async throws -> [Memo]
273
+
266
274
  /// Get all memos for `transaction`.
267
275
  ///
268
276
  // sourcery: mockedName="getMemosForClearedTransaction"
@@ -304,6 +312,9 @@ public protocol Synchronizer: AnyObject {
304
312
  /// - Returns: `AccountBalance`, struct that holds sapling and unshielded balances or `nil` when no account is associated with `accountIndex`
305
313
  func getAccountBalance(accountIndex: Int) async throws -> AccountBalance?
306
314
 
315
+ /// Fetches the latest ZEC-USD exchange rate and updates `exchangeRateUSDSubject`.
316
+ func refreshExchangeRateUSD()
317
+
307
318
  /// Rescans the known blocks with the current keys.
308
319
  ///
309
320
  /// `rewind(policy:)` can be called anytime. If the sync process is in progress then it is stopped first. In this case, it make some significant
@@ -356,6 +367,23 @@ public protocol Synchronizer: AnyObject {
356
367
  ///
357
368
  /// - parameter seed: byte array of the seed
358
369
  func isSeedRelevantToAnyDerivedAccount(seed: [UInt8]) async throws -> Bool
370
+
371
+ /// Takes the list of endpoints and runs it through a series of checks to evaluate its performance.
372
+ /// - Parameters:
373
+ /// - endpoints: Array of endpoints to evaluate.
374
+ /// - latencyThresholdMillis: The mean latency of `getInfo` and `getTheLatestHeight` calls must be below this threshold. The default is 300 ms.
375
+ /// - fetchThresholdSeconds: The time to download `nBlocksToFetch` blocks from the stream must be below this threshold. The default is 60 seconds.
376
+ /// - nBlocksToFetch: The number of blocks expected to be downloaded from the stream, with the time compared to `fetchThresholdSeconds`. The default is 100.
377
+ /// - kServers: The expected number of endpoints in the output. The default is 3.
378
+ /// - network: Mainnet or testnet. The default is mainnet.
379
+ func evaluateBestOf(
380
+ endpoints: [LightWalletEndpoint],
381
+ latencyThresholdMillis: Double,
382
+ fetchThresholdSeconds: Double,
383
+ nBlocksToFetch: UInt64,
384
+ kServers: Int,
385
+ network: NetworkType
386
+ ) async -> [LightWalletEndpoint]
359
387
  }
360
388
 
361
389
  public enum SyncStatus: Equatable {
@@ -108,15 +108,29 @@ extension DerivationTool: KeyValidation {
108
108
  }
109
109
 
110
110
  public func isValidUnifiedAddress(_ unifiedAddress: String) -> Bool {
111
- backend.isValidUnifiedAddress(unifiedAddress)
111
+ DerivationTool.getAddressMetadata(unifiedAddress).map {
112
+ $0.networkType == backend.networkType && $0.addressType == AddressType.unified
113
+ } ?? false
112
114
  }
113
-
115
+
114
116
  public func isValidTransparentAddress(_ tAddress: String) -> Bool {
115
- backend.isValidTransparentAddress(tAddress)
117
+ DerivationTool.getAddressMetadata(tAddress).map {
118
+ $0.networkType == backend.networkType && (
119
+ $0.addressType == AddressType.p2pkh || $0.addressType == AddressType.p2sh
120
+ )
121
+ } ?? false
116
122
  }
117
-
123
+
118
124
  public func isValidSaplingAddress(_ zAddress: String) -> Bool {
119
- backend.isValidSaplingAddress(zAddress)
125
+ DerivationTool.getAddressMetadata(zAddress).map {
126
+ $0.networkType == backend.networkType && $0.addressType == AddressType.sapling
127
+ } ?? false
128
+ }
129
+
130
+ public func isValidTexAddress(_ texAddress: String) -> Bool {
131
+ DerivationTool.getAddressMetadata(texAddress).map {
132
+ $0.networkType == backend.networkType && $0.addressType == AddressType.tex
133
+ } ?? false
120
134
  }
121
135
 
122
136
  public func isValidSaplingExtendedSpendingKey(_ extsk: String) -> Bool {
@@ -155,6 +169,16 @@ extension UnifiedAddress {
155
169
  }
156
170
  }
157
171
 
172
+ extension TexAddress {
173
+ /// This constructor is for internal use for Strings encodings that are assumed to be
174
+ /// already validated by another function. only for internal use. Unless you are
175
+ /// constructing an address from a primitive function of the FFI, you probably
176
+ /// shouldn't be using this.
177
+ init(validatedEncoding: String) {
178
+ self.encoding = validatedEncoding
179
+ }
180
+ }
181
+
158
182
  extension UnifiedFullViewingKey {
159
183
  /// This constructor is for internal use for Strings encodings that are assumed to be
160
184
  /// already validated by another function. only for internal use. Unless you are
@@ -0,0 +1,57 @@
1
+ //
2
+ // TorRuntime.swift
3
+ //
4
+ //
5
+ // Created by Jack Grigg on 04/06/2024.
6
+ //
7
+
8
+ import Foundation
9
+
10
+
11
+ public class TorClient {
12
+ private let runtime: OpaquePointer
13
+ public var cachedFiatCurrencyResult: FiatCurrencyResult?
14
+
15
+ init(torDir: URL) async throws {
16
+ // Ensure that the directory exists.
17
+ let fileManager = FileManager()
18
+ if !fileManager.fileExists(atPath: torDir.path) {
19
+ do {
20
+ try fileManager.createDirectory(at: torDir, withIntermediateDirectories: true)
21
+ } catch {
22
+ throw ZcashError.blockRepositoryCreateBlocksCacheDirectory(torDir, error)
23
+ }
24
+ }
25
+
26
+ let rawDir = torDir.osPathStr()
27
+ let runtimePtr = zcashlc_create_tor_runtime(rawDir.0, rawDir.1)
28
+
29
+ guard let runtimePtr else {
30
+ throw ZcashError.rustTorClientInit(lastErrorMessage(fallback: "`TorClient` init failed with unknown error"))
31
+ }
32
+
33
+ runtime = runtimePtr
34
+ }
35
+
36
+ deinit {
37
+ zcashlc_free_tor_runtime(runtime)
38
+ }
39
+
40
+ public func getExchangeRateUSD() async throws -> FiatCurrencyResult {
41
+ let rate = zcashlc_get_exchange_rate_usd(runtime)
42
+
43
+ if rate.is_sign_negative {
44
+ throw ZcashError.rustTorClientGet(lastErrorMessage(fallback: "`TorClient.get` failed with unknown error"))
45
+ }
46
+
47
+ let newValue = FiatCurrencyResult(
48
+ date: Date(),
49
+ rate: NSDecimalNumber(mantissa: rate.mantissa, exponent: rate.exponent, isNegative: rate.is_sign_negative),
50
+ state: .success
51
+ )
52
+
53
+ cachedFiatCurrencyResult = newValue
54
+
55
+ return newValue
56
+ }
57
+ }
@@ -11,6 +11,7 @@ import Foundation
11
11
  Represents what's expected from a logging entity
12
12
  */
13
13
  public protocol Logger {
14
+ func maxLogLevel() -> OSLogger.LogLevel?
14
15
  func debug(_ message: String, file: StaticString, function: StaticString, line: Int)
15
16
  func info(_ message: String, file: StaticString, function: StaticString, line: Int)
16
17
  func event(_ message: String, file: StaticString, function: StaticString, line: Int)
@@ -44,6 +45,9 @@ extension Logger {
44
45
  A concrete logger implementation that logs nothing at all
45
46
  */
46
47
  struct NullLogger: Logger {
48
+ func maxLogLevel() -> OSLogger.LogLevel? {
49
+ nil
50
+ }
47
51
  func debug(_ message: String, file: StaticString, function: StaticString, line: Int) {}
48
52
  func info(_ message: String, file: StaticString, function: StaticString, line: Int) {}
49
53
  func event(_ message: String, file: StaticString, function: StaticString, line: Int) {}
@@ -39,6 +39,10 @@ public class OSLogger: Logger {
39
39
  }
40
40
  }
41
41
 
42
+ public func maxLogLevel() -> LogLevel? {
43
+ self.level
44
+ }
45
+
42
46
  public func debug(
43
47
  _ message: String,
44
48
  file: StaticString = #file,