ZcashLightClientKit/Sources/ZcashLightClientKit/Service/LightWalletGRPCService.swift

551 lines
18 KiB
Swift
Raw Normal View History

//
// ServiceHelper.swift
// gRPC-PoC
//
// Created by Francisco Gindre on 29/08/2019.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
import GRPC
import NIO
import NIOHPACK
2021-09-17 06:49:58 -07:00
public typealias Channel = GRPC.GRPCChannel
public protocol LightWalletdInfo {
var version: String { get }
var vendor: String { get }
/// true
var taddrSupport: Bool { get }
/// either "main" or "test"
var chainName: String { get }
/// depends on mainnet or testnet
var saplingActivationHeight: UInt64 { get }
/// protocol identifier, see consensus/upgrades.cpp
var consensusBranchID: String { get }
/// latest block on the best chain
var blockHeight: UInt64 { get }
var gitCommit: String { get }
var branch: String { get }
var buildDate: String { get }
var buildUser: String { get }
/// less than tip height if zcashd is syncing
var estimatedHeight: UInt64 { get }
/// example: "v4.1.1-877212414"
var zcashdBuild: String { get }
/// example: "/MagicBean:4.1.1/"
var zcashdSubversion: String { get }
}
2021-09-17 06:49:58 -07:00
extension LightdInfo: LightWalletdInfo {}
2021-09-17 06:49:58 -07:00
/**
Swift GRPC implementation of Lightwalletd service
*/
public enum GRPCResult: Equatable {
2021-09-17 06:49:58 -07:00
case success
case error(_ error: LightWalletServiceError)
}
public protocol CancellableCall {
func cancel()
}
extension ServerStreamingCall: CancellableCall {
public func cancel() {
self.cancel(promise: self.eventLoop.makePromise(of: Void.self))
}
}
2021-09-17 06:49:58 -07:00
public struct BlockProgress: Equatable {
public var startHeight: BlockHeight
public var targetHeight: BlockHeight
public var progressHeight: BlockHeight
2021-09-17 06:49:58 -07:00
public var progress: Float {
let overall = self.targetHeight - self.startHeight
return overall > 0 ? Float((self.progressHeight - self.startHeight)) / Float(overall) : 0
}
2021-06-14 16:38:05 -07:00
}
public extension BlockProgress {
static let nullProgress = BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)
}
2019-10-31 15:43:09 -07:00
public class LightWalletGRPCService {
let channel: Channel
2021-09-17 06:49:58 -07:00
let connectionManager: ConnectionStatusManager
let compactTxStreamer: CompactTxStreamerClient
let singleCallTimeout: TimeLimit
let streamingCallTimeout: TimeLimit
2021-09-17 06:49:58 -07:00
var queue: DispatchQueue
public convenience init(endpoint: LightWalletEndpoint) {
2021-09-17 06:49:58 -07:00
self.init(
host: endpoint.host,
port: endpoint.port,
secure: endpoint.secure,
singleCallTimeout: endpoint.singleCallTimeoutInMillis,
streamingCallTimeout: endpoint.streamingCallTimeoutInMillis
)
}
2021-09-17 06:49:58 -07:00
public init(host: String, port: Int = 9067, secure: Bool = true, singleCallTimeout: Int64 = 10000, streamingCallTimeout: Int64 = 10000) {
2021-09-17 06:49:58 -07:00
self.connectionManager = ConnectionStatusManager()
self.queue = DispatchQueue.init(label: "LightWalletGRPCService")
self.streamingCallTimeout = TimeLimit.timeout(.milliseconds(streamingCallTimeout))
self.singleCallTimeout = TimeLimit.timeout(.milliseconds(singleCallTimeout))
2021-09-17 06:49:58 -07:00
let connectionBuilder = secure ?
ClientConnection.usingPlatformAppropriateTLS(for: MultiThreadedEventLoopGroup(numberOfThreads: 1)) :
ClientConnection.insecure(group: MultiThreadedEventLoopGroup(numberOfThreads: 1))
let channel = connectionBuilder
.withConnectivityStateDelegate(connectionManager, executingOn: queue)
.connect(host: host, port: port)
2021-09-17 06:49:58 -07:00
self.channel = channel
2021-09-17 06:49:58 -07:00
compactTxStreamer = CompactTxStreamerClient(
channel: self.channel,
defaultCallOptions: Self.callOptions(
timeLimit: self.singleCallTimeout
2021-09-17 06:49:58 -07:00
)
)
2019-10-31 15:43:09 -07:00
}
2021-09-17 06:49:58 -07:00
deinit {
_ = channel.close()
_ = compactTxStreamer.channel.close()
}
func stop() {
_ = channel.close()
}
func blockRange(startHeight: BlockHeight, endHeight: BlockHeight? = nil, result: @escaping (CompactBlock) -> Void) throws -> ServerStreamingCall<BlockRange, CompactBlock> {
compactTxStreamer.getBlockRange(BlockRange(startHeight: startHeight, endHeight: endHeight), handler: result)
}
func latestBlock() throws -> BlockID {
try compactTxStreamer.getLatestBlock(ChainSpec()).response.wait()
}
func getTx(hash: String) throws -> RawTransaction {
var filter = TxFilter()
filter.hash = Data(hash.utf8)
2021-09-17 06:49:58 -07:00
return try compactTxStreamer.getTransaction(filter).response.wait()
}
static func callOptions(timeLimit: TimeLimit) -> CallOptions {
2021-09-17 06:49:58 -07:00
CallOptions(
customMetadata: HPACKHeaders(),
timeLimit: timeLimit,
messageEncoding: .disabled,
requestIDProvider: .autogenerated,
requestIDHeader: nil,
cacheable: false
)
}
}
extension LightWalletGRPCService: LightWalletService {
2021-09-17 06:49:58 -07:00
@discardableResult
public func blockStream(
2021-09-15 05:21:29 -07:00
startHeight: BlockHeight,
endHeight: BlockHeight,
result: @escaping (Result<GRPCResult, LightWalletServiceError>) -> Void,
handler: @escaping (ZcashCompactBlock) -> Void,
2021-09-17 06:49:58 -07:00
progress: @escaping (BlockProgress) -> Void
2021-09-15 05:21:29 -07:00
) -> CancellableCall {
let future = compactTxStreamer.getBlockRange(
BlockRange(
startHeight: startHeight,
2021-09-17 06:49:58 -07:00
endHeight: endHeight
),
2021-09-15 05:21:29 -07:00
callOptions: Self.callOptions(timeLimit: self.streamingCallTimeout),
handler: { compactBlock in
handler(ZcashCompactBlock(compactBlock: compactBlock))
2021-09-17 06:49:58 -07:00
progress(
BlockProgress(
startHeight: startHeight,
targetHeight: endHeight,
progressHeight: BlockHeight(compactBlock.height)
2021-09-15 05:21:29 -07:00
)
)
}
)
2021-09-15 05:21:29 -07:00
future.status.whenComplete { completionResult in
switch completionResult {
case .success(let status):
switch status.code {
case .ok:
2021-09-17 06:49:58 -07:00
result(.success(GRPCResult.success))
default:
result(.failure(LightWalletServiceError.mapCode(status)))
}
case .failure(let error):
result(.failure(LightWalletServiceError.genericError(error: error)))
}
}
return future
}
public func getInfo() throws -> LightWalletdInfo {
try compactTxStreamer.getLightdInfo(Empty()).response.wait()
}
public func getInfo(result: @escaping (Result<LightWalletdInfo, LightWalletServiceError>) -> Void) {
2021-09-15 05:21:29 -07:00
compactTxStreamer.getLightdInfo(Empty()).response.whenComplete { completionResult in
switch completionResult {
case .success(let info):
result(.success(info))
case .failure(let error):
result(.failure(error.mapToServiceError()))
}
}
}
public func closeConnection() {
2021-05-07 11:50:50 -07:00
_ = channel.close()
}
public func fetchTransaction(txId: Data) throws -> TransactionEntity {
var txFilter = TxFilter()
txFilter.hash = txId
do {
let rawTx = try compactTxStreamer.getTransaction(txFilter).response.wait()
return TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)
} catch {
throw error.mapToServiceError()
}
}
public func fetchTransaction(txId: Data, result: @escaping (Result<TransactionEntity, LightWalletServiceError>) -> Void) {
var txFilter = TxFilter()
txFilter.hash = txId
2021-09-15 05:21:29 -07:00
compactTxStreamer.getTransaction(txFilter).response.whenComplete { response in
switch response {
case .failure(let error):
result(.failure(error.mapToServiceError()))
case .success(let rawTx):
result(.success(TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)))
}
2021-09-15 05:21:29 -07:00
}
}
public func submit(spendTransaction: Data, result: @escaping (Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void) {
do {
2021-09-17 06:49:58 -07:00
let transaction = try RawTransaction(serializedData: spendTransaction)
let response = self.compactTxStreamer.sendTransaction(transaction).response
2021-09-15 05:21:29 -07:00
response.whenComplete { responseResult in
switch responseResult {
2021-09-15 05:21:29 -07:00
case .failure(let error):
result(.failure(LightWalletServiceError.sentFailed(error: error)))
case .success(let success):
result(.success(success))
}
}
} catch {
result(.failure(error.mapToServiceError()))
}
}
public func submit(spendTransaction: Data) throws -> LightWalletServiceResponse {
2021-09-15 05:21:29 -07:00
let rawTx = RawTransaction.with { raw in
raw.data = spendTransaction
}
do {
return try compactTxStreamer.sendTransaction(rawTx).response.wait()
} catch {
throw error.mapToServiceError()
}
}
2019-10-31 15:43:09 -07:00
public func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] {
2021-09-17 06:49:58 -07:00
var blocks: [CompactBlock] = []
2021-09-17 06:49:58 -07:00
let response = compactTxStreamer.getBlockRange(
range.blockRange(),
handler: { blocks.append($0) }
2021-09-15 05:21:29 -07:00
)
let status = try response.status.wait()
2021-09-17 06:49:58 -07:00
switch status.code {
2021-09-15 05:21:29 -07:00
case .ok:
2021-09-17 06:49:58 -07:00
return blocks.asZcashCompactBlocks()
2021-09-15 05:21:29 -07:00
default:
2021-09-17 06:49:58 -07:00
throw LightWalletServiceError.mapCode(status)
}
}
2019-10-31 15:43:09 -07:00
public func latestBlockHeight(result: @escaping (Result<BlockHeight, LightWalletServiceError>) -> Void) {
let response = compactTxStreamer.getLatestBlock(ChainSpec()).response
response.whenSuccessBlocking(onto: queue) { blockID in
guard let blockHeight = Int(exactly: blockID.height) else {
result(.failure(LightWalletServiceError.generalError(message: "error creating blockheight from BlockID \(blockID)")))
return
}
result(.success(blockHeight))
}
response.whenFailureBlocking(onto: queue) { error in
result(.failure(error.mapToServiceError()))
}
}
2019-10-31 15:43:09 -07:00
public func blockRange(_ range: CompactBlockRange, result: @escaping (Result<[ZcashCompactBlock], LightWalletServiceError>) -> Void) {
queue.async { [weak self] in
guard let self = self else { return }
2021-09-17 06:49:58 -07:00
var blocks: [CompactBlock] = []
let response = self.compactTxStreamer.getBlockRange(range.blockRange(), handler: { blocks.append($0) })
2021-09-17 06:49:58 -07:00
do {
let status = try response.status.wait()
switch status.code {
case .ok:
result(.success(blocks.asZcashCompactBlocks()))
default:
result(.failure(.mapCode(status)))
}
} catch {
result(.failure(error.mapToServiceError()))
}
}
}
2019-10-31 15:43:09 -07:00
public func latestBlockHeight() throws -> BlockHeight {
guard let height = try? latestBlock().compactBlockHeight() else {
throw LightWalletServiceError.timeOut
}
return height
}
2020-12-09 15:57:23 -08:00
2021-07-26 16:22:30 -07:00
public func fetchUTXOs(for tAddress: String, height: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
2021-09-15 05:21:29 -07:00
let arg = GetAddressUtxosArg.with { utxoArgs in
2021-04-12 13:28:33 -07:00
utxoArgs.addresses = [tAddress]
2021-04-01 07:27:26 -07:00
utxoArgs.startHeight = UInt64(height)
}
do {
return try self.compactTxStreamer.getAddressUtxos(arg).response.wait().addressUtxos.map { reply in
2021-09-15 05:21:29 -07:00
UTXO(
id: nil,
address: tAddress,
prevoutTxId: reply.txid,
prevoutIndex: Int(reply.index),
script: reply.script,
valueZat: Int(reply.valueZat),
height: Int(reply.height),
spentInTx: nil
2021-04-01 07:27:26 -07:00
)
}
} catch {
throw error.mapToServiceError()
}
}
2021-07-26 16:22:30 -07:00
public func fetchUTXOs(for tAddress: String, height: BlockHeight, result: @escaping (Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void) {
2020-12-09 15:57:23 -08:00
queue.async { [weak self] in
guard let self = self else { return }
2021-09-15 05:21:29 -07:00
let arg = GetAddressUtxosArg.with { utxoArgs in
2021-04-12 13:28:33 -07:00
utxoArgs.addresses = [tAddress]
utxoArgs.startHeight = UInt64(height)
2020-12-09 15:57:23 -08:00
}
2021-09-17 06:49:58 -07:00
var utxos: [UnspentTransactionOutputEntity] = []
2021-09-15 05:21:29 -07:00
let response = self.compactTxStreamer.getAddressUtxosStream(arg) { reply in
2020-12-09 15:57:23 -08:00
utxos.append(
2021-09-15 05:21:29 -07:00
UTXO(
id: nil,
address: tAddress,
prevoutTxId: reply.txid,
prevoutIndex: Int(reply.index),
script: reply.script,
valueZat: Int(reply.valueZat),
height: Int(reply.height),
spentInTx: nil
2020-12-09 15:57:23 -08:00
)
)
}
do {
let status = try response.status.wait()
switch status.code {
case .ok:
result(.success(utxos))
default:
result(.failure(.mapCode(status)))
}
} catch {
result(.failure(error.mapToServiceError()))
}
}
}
2021-04-01 07:27:26 -07:00
2021-07-26 16:22:30 -07:00
public func fetchUTXOs(for tAddresses: [String], height: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
2021-09-15 05:21:29 -07:00
guard !tAddresses.isEmpty else {
2021-04-01 07:27:26 -07:00
return [] // FIXME: throw a real error
}
2021-09-17 06:49:58 -07:00
var utxos: [UnspentTransactionOutputEntity] = []
2021-04-01 07:27:26 -07:00
2021-09-15 05:21:29 -07:00
let arg = GetAddressUtxosArg.with { utxoArgs in
2021-04-12 13:28:33 -07:00
utxoArgs.addresses = tAddresses
utxoArgs.startHeight = UInt64(height)
2021-04-01 07:27:26 -07:00
}
2021-09-15 05:21:29 -07:00
utxos.append(
contentsOf:
try self.compactTxStreamer.getAddressUtxos(arg).response.wait().addressUtxos.map { reply in
UTXO(
id: nil,
address: reply.address,
prevoutTxId: reply.txid,
prevoutIndex: Int(reply.index),
script: reply.script,
valueZat: Int(reply.valueZat),
height: Int(reply.height),
spentInTx: nil
)
}
2021-04-12 13:28:33 -07:00
)
2021-04-01 07:27:26 -07:00
return utxos
}
2021-09-17 06:49:58 -07:00
public func fetchUTXOs(
for tAddresses: [String],
height: BlockHeight,
result: @escaping (Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void
) {
2021-09-15 05:21:29 -07:00
guard !tAddresses.isEmpty else {
2021-04-01 07:27:26 -07:00
return result(.success([])) // FIXME: throw a real error
}
2021-09-17 06:49:58 -07:00
var utxos: [UnspentTransactionOutputEntity] = []
2021-04-01 07:27:26 -07:00
self.queue.async { [weak self] in
guard let self = self else { return }
2021-09-17 06:49:58 -07:00
let args = GetAddressUtxosArg.with { utxoArgs in
2021-04-12 13:28:33 -07:00
utxoArgs.addresses = tAddresses
utxoArgs.startHeight = UInt64(height)
}
2021-04-01 07:27:26 -07:00
do {
2021-04-12 13:28:33 -07:00
let response = try self.compactTxStreamer.getAddressUtxosStream(args) { reply in
2021-09-17 06:49:58 -07:00
utxos.append(
UTXO(
id: nil,
address: reply.address,
prevoutTxId: reply.txid,
prevoutIndex: Int(reply.index),
script: reply.script,
valueZat: Int(reply.valueZat),
height: Int(reply.height),
spentInTx: nil
2021-04-12 13:28:33 -07:00
)
2021-09-17 06:49:58 -07:00
)
2021-09-15 05:21:29 -07:00
}
.status
.wait()
2021-09-17 06:49:58 -07:00
2021-04-02 15:18:16 -07:00
switch response.code {
case .ok:
result(.success(utxos))
default:
result(.failure(.mapCode(response)))
}
2021-04-01 07:27:26 -07:00
} catch {
result(.failure(error.mapToServiceError()))
}
}
}
}
2021-09-17 06:49:58 -07:00
// MARK: - Extensions
extension Notification.Name {
static let connectionStatusChanged = Notification.Name("LightWalletServiceConnectivityStatusChanged")
}
extension TimeAmount {
static let singleCallTimeout = TimeAmount.seconds(30)
static let streamingCallTimeout = TimeAmount.minutes(10)
}
extension CallOptions {
static var lwdCall: CallOptions {
CallOptions(
customMetadata: HPACKHeaders(),
timeLimit: .timeout(.singleCallTimeout),
messageEncoding: .disabled,
requestIDProvider: .autogenerated,
requestIDHeader: nil,
cacheable: false
)
}
}
extension Error {
func mapToServiceError() -> LightWalletServiceError {
2021-09-17 06:49:58 -07:00
guard let grpcError = self as? GRPCStatusTransformable else {
return LightWalletServiceError.genericError(error: self)
}
return LightWalletServiceError.mapCode(grpcError.makeGRPCStatus())
}
}
extension LightWalletServiceError {
static func mapCode(_ status: GRPCStatus) -> LightWalletServiceError {
switch status.code {
case .ok:
2021-09-17 06:49:58 -07:00
return LightWalletServiceError.unknown
case .cancelled:
return LightWalletServiceError.userCancelled
case .unknown:
2021-04-08 10:18:16 -07:00
return LightWalletServiceError.generalError(message: status.message ?? "GRPC unknown error contains no message")
case .deadlineExceeded:
return LightWalletServiceError.timeOut
default:
return LightWalletServiceError.genericError(error: status)
}
}
}
class ConnectionStatusManager: ConnectivityStateDelegate {
func connectivityStateDidChange(from oldState: ConnectivityState, to newState: ConnectivityState) {
LoggerProxy.event("Connection Changed from \(oldState) to \(newState)")
2021-06-14 16:38:05 -07:00
NotificationCenter.default.post(
name: .blockProcessorConnectivityStateChanged,
object: self,
userInfo: [
2021-09-15 05:21:29 -07:00
CompactBlockProcessorNotificationKey.currentConnectivityStatus: newState,
CompactBlockProcessorNotificationKey.previousConnectivityStatus: oldState
]
)
}
}