2019-10-18 11:45:19 -07:00
|
|
|
//
|
|
|
|
// ServiceHelper.swift
|
|
|
|
// gRPC-PoC
|
|
|
|
//
|
|
|
|
// Created by Francisco Gindre on 29/08/2019.
|
|
|
|
// Copyright © 2019 Electric Coin Company. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
2020-04-09 15:25:43 -07:00
|
|
|
import GRPC
|
|
|
|
import NIO
|
2020-08-10 15:19:59 -07:00
|
|
|
import NIOHPACK
|
2022-10-03 11:54:43 -07:00
|
|
|
import NIOTransportServices
|
2019-10-18 11:45:19 -07:00
|
|
|
|
2021-09-17 06:49:58 -07:00
|
|
|
public typealias Channel = GRPC.GRPCChannel
|
2021-05-17 14:14:59 -07:00
|
|
|
|
|
|
|
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
|
|
|
|
2021-05-17 14:14:59 -07:00
|
|
|
extension LightdInfo: LightWalletdInfo {}
|
2021-05-28 15:45:18 -07:00
|
|
|
|
2021-09-17 06:49:58 -07:00
|
|
|
/**
|
|
|
|
Swift GRPC implementation of Lightwalletd service
|
|
|
|
*/
|
2021-05-28 15:45:18 -07:00
|
|
|
public enum GRPCResult: Equatable {
|
2021-09-17 06:49:58 -07:00
|
|
|
case success
|
2021-05-28 15:45:18 -07:00
|
|
|
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-05-28 15:45:18 -07:00
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-03-04 12:09:39 -08: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 {
|
2019-10-18 11:45:19 -07:00
|
|
|
let channel: Channel
|
2021-09-17 06:49:58 -07:00
|
|
|
let connectionManager: ConnectionStatusManager
|
2022-08-25 06:39:59 -07:00
|
|
|
let compactTxStreamer: CompactTxStreamerNIOClient
|
|
|
|
let compactTxStreamerAsync: CompactTxStreamerAsyncClient
|
2021-05-28 15:45:18 -07:00
|
|
|
let singleCallTimeout: TimeLimit
|
|
|
|
let streamingCallTimeout: TimeLimit
|
2021-09-17 06:49:58 -07:00
|
|
|
|
|
|
|
var queue: DispatchQueue
|
2019-10-18 11:45:19 -07:00
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
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-06-03 07:51:12 -07:00
|
|
|
}
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2022-07-29 06:07:08 -07:00
|
|
|
/// Inits a connection to a Lightwalletd service to the given
|
|
|
|
/// - Parameters:
|
|
|
|
/// - host: the hostname of the lightwalletd server
|
|
|
|
/// - port: port of the server. Default is 9067
|
|
|
|
/// - secure: whether this server is TLS or plaintext. default True (TLS)
|
|
|
|
/// - singleCallTimeout: Timeout for unary calls in milliseconds.
|
|
|
|
/// - streamingCallTimeout: Timeout for streaming calls in milliseconds.
|
|
|
|
public init(
|
|
|
|
host: String,
|
|
|
|
port: Int = 9067,
|
|
|
|
secure: Bool = true,
|
|
|
|
singleCallTimeout: Int64,
|
|
|
|
streamingCallTimeout: Int64
|
|
|
|
) {
|
2021-09-17 06:49:58 -07:00
|
|
|
self.connectionManager = ConnectionStatusManager()
|
2021-04-20 15:30:59 -07:00
|
|
|
self.queue = DispatchQueue.init(label: "LightWalletGRPCService")
|
2021-05-28 15:45:18 -07:00
|
|
|
self.streamingCallTimeout = TimeLimit.timeout(.milliseconds(streamingCallTimeout))
|
|
|
|
self.singleCallTimeout = TimeLimit.timeout(.milliseconds(singleCallTimeout))
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2022-07-08 12:49:24 -07:00
|
|
|
let connectionBuilder = secure ?
|
2022-10-03 11:54:43 -07:00
|
|
|
ClientConnection.usingPlatformAppropriateTLS(for: NIOTSEventLoopGroup(loopCount: 1, defaultQoS: .default)) :
|
|
|
|
ClientConnection.insecure(group: NIOTSEventLoopGroup(loopCount: 1, defaultQoS: .default))
|
2022-07-08 12:49:24 -07:00
|
|
|
|
|
|
|
let channel = connectionBuilder
|
|
|
|
.withConnectivityStateDelegate(connectionManager, executingOn: queue)
|
|
|
|
.connect(host: host, port: port)
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2021-04-20 15:30:59 -07:00
|
|
|
self.channel = channel
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
compactTxStreamer = CompactTxStreamerNIOClient(
|
|
|
|
channel: self.channel,
|
|
|
|
defaultCallOptions: Self.callOptions(
|
|
|
|
timeLimit: self.singleCallTimeout
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
compactTxStreamerAsync = CompactTxStreamerAsyncClient(
|
2021-09-17 06:49:58 -07:00
|
|
|
channel: self.channel,
|
|
|
|
defaultCallOptions: Self.callOptions(
|
2022-07-14 16:19:46 -07:00
|
|
|
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()
|
2022-08-25 06:39:59 -07:00
|
|
|
_ = compactTxStreamerAsync.channel.close()
|
2021-09-17 06:49:58 -07:00
|
|
|
}
|
|
|
|
|
2020-03-13 17:00:01 -07:00
|
|
|
func stop() {
|
2020-04-09 15:25:43 -07:00
|
|
|
_ = channel.close()
|
2020-03-13 17:00:01 -07:00
|
|
|
}
|
|
|
|
|
2020-04-09 15:25:43 -07:00
|
|
|
func blockRange(startHeight: BlockHeight, endHeight: BlockHeight? = nil, result: @escaping (CompactBlock) -> Void) throws -> ServerStreamingCall<BlockRange, CompactBlock> {
|
|
|
|
compactTxStreamer.getBlockRange(BlockRange(startHeight: startHeight, endHeight: endHeight), handler: result)
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
2022-08-25 06:39:59 -07:00
|
|
|
|
2019-10-18 11:45:19 -07:00
|
|
|
func latestBlock() throws -> BlockID {
|
2020-04-09 15:25:43 -07:00
|
|
|
try compactTxStreamer.getLatestBlock(ChainSpec()).response.wait()
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
|
|
|
|
2019-10-18 13:09:13 -07:00
|
|
|
func getTx(hash: String) throws -> RawTransaction {
|
2019-10-18 11:45:19 -07:00
|
|
|
var filter = TxFilter()
|
|
|
|
filter.hash = Data(hash.utf8)
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2020-04-09 15:25:43 -07:00
|
|
|
return try compactTxStreamer.getTransaction(filter).response.wait()
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
|
|
|
|
2021-05-28 15:45:18 -07:00
|
|
|
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
|
|
|
|
)
|
2021-05-28 15:45:18 -07:00
|
|
|
}
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
// MARK: - LightWalletServiceBlockingAPI
|
|
|
|
|
|
|
|
extension LightWalletGRPCService: LightWalletServiceBlockingAPI {
|
2021-05-17 14:14:59 -07:00
|
|
|
public func getInfo() throws -> LightWalletdInfo {
|
|
|
|
try compactTxStreamer.getLightdInfo(Empty()).response.wait()
|
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func latestBlockHeight() throws -> BlockHeight {
|
|
|
|
guard let height = try? compactTxStreamer.getLatestBlock(ChainSpec()).response.wait().compactBlockHeight() else {
|
|
|
|
throw LightWalletServiceError.timeOut
|
2021-05-17 14:14:59 -07:00
|
|
|
}
|
2022-08-25 06:39:59 -07:00
|
|
|
return height
|
2021-05-17 14:14:59 -07:00
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] {
|
|
|
|
var blocks: [CompactBlock] = []
|
|
|
|
|
|
|
|
let response = compactTxStreamer.getBlockRange(
|
|
|
|
range.blockRange(),
|
|
|
|
handler: { blocks.append($0) }
|
|
|
|
)
|
|
|
|
|
|
|
|
let status = try response.status.wait()
|
|
|
|
|
|
|
|
switch status.code {
|
|
|
|
case .ok:
|
|
|
|
return blocks.asZcashCompactBlocks()
|
|
|
|
default:
|
|
|
|
throw LightWalletServiceError.mapCode(status)
|
|
|
|
}
|
2021-04-21 13:02:23 -07:00
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func submit(spendTransaction: Data) throws -> LightWalletServiceResponse {
|
|
|
|
let rawTx = RawTransaction.with { raw in
|
|
|
|
raw.data = spendTransaction
|
|
|
|
}
|
2020-08-10 15:19:59 -07:00
|
|
|
do {
|
2022-08-25 06:39:59 -07:00
|
|
|
return try compactTxStreamer.sendTransaction(rawTx).response.wait()
|
2020-08-10 15:19:59 -07:00
|
|
|
} catch {
|
|
|
|
throw error.mapToServiceError()
|
|
|
|
}
|
2020-04-23 10:11:03 -07:00
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func fetchTransaction(txId: Data) throws -> TransactionEntity {
|
2020-04-23 10:11:03 -07:00
|
|
|
var txFilter = TxFilter()
|
|
|
|
txFilter.hash = txId
|
|
|
|
|
2020-04-09 15:25:43 -07:00
|
|
|
do {
|
2022-08-25 06:39:59 -07:00
|
|
|
let rawTx = try compactTxStreamer.getTransaction(txFilter).response.wait()
|
2019-12-03 07:19:44 -08:00
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
return TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)
|
2020-04-09 15:25:43 -07:00
|
|
|
} catch {
|
2022-08-25 06:39:59 -07:00
|
|
|
throw error.mapToServiceError()
|
2019-12-03 07:19:44 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func fetchUTXOs(for tAddress: String, height: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
|
|
|
|
let arg = GetAddressUtxosArg.with { utxoArgs in
|
|
|
|
utxoArgs.addresses = [tAddress]
|
|
|
|
utxoArgs.startHeight = UInt64(height)
|
2019-12-16 14:25:45 -08:00
|
|
|
}
|
2020-08-10 15:19:59 -07:00
|
|
|
do {
|
2022-08-25 06:39:59 -07:00
|
|
|
return try self.compactTxStreamer.getAddressUtxos(arg).response.wait().addressUtxos.map { reply in
|
|
|
|
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-08-10 15:19:59 -07:00
|
|
|
} catch {
|
|
|
|
throw error.mapToServiceError()
|
|
|
|
}
|
2019-12-03 07:19:44 -08:00
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func fetchUTXOs(for tAddresses: [String], height: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
|
|
|
|
guard !tAddresses.isEmpty else {
|
|
|
|
return [] // FIXME: throw a real error
|
|
|
|
}
|
2019-10-18 11:45:19 -07:00
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
var utxos: [UnspentTransactionOutputEntity] = []
|
2019-10-18 11:45:19 -07:00
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
let arg = GetAddressUtxosArg.with { utxoArgs in
|
|
|
|
utxoArgs.addresses = tAddresses
|
|
|
|
utxoArgs.startHeight = UInt64(height)
|
|
|
|
}
|
|
|
|
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
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
return utxos
|
|
|
|
}
|
|
|
|
}
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
// MARK: - LightWalletServiceNonBlockingAPI
|
|
|
|
|
|
|
|
extension LightWalletGRPCService: LightWalletServiceNonBlockingAPI {
|
|
|
|
public func getInfo(result: @escaping (Result<LightWalletdInfo, LightWalletServiceError>) -> Void) {
|
|
|
|
compactTxStreamer.getLightdInfo(Empty()).response.whenComplete { completionResult in
|
|
|
|
switch completionResult {
|
|
|
|
case .success(let info):
|
|
|
|
result(.success(info))
|
|
|
|
case .failure(let error):
|
|
|
|
result(.failure(error.mapToServiceError()))
|
|
|
|
}
|
2020-04-09 15:25:43 -07:00
|
|
|
}
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func getInfoAsync() async throws -> LightWalletdInfo {
|
|
|
|
try await compactTxStreamerAsync.getLightdInfo(Empty())
|
|
|
|
}
|
|
|
|
|
2019-10-31 15:43:09 -07:00
|
|
|
public func latestBlockHeight(result: @escaping (Result<BlockHeight, LightWalletServiceError>) -> Void) {
|
2020-04-09 15:25:43 -07:00
|
|
|
let response = compactTxStreamer.getLatestBlock(ChainSpec()).response
|
|
|
|
|
2021-04-21 13:02:23 -07:00
|
|
|
response.whenSuccessBlocking(onto: queue) { blockID in
|
2020-04-09 15:25:43 -07:00
|
|
|
guard let blockHeight = Int(exactly: blockID.height) else {
|
2020-08-10 15:19:59 -07:00
|
|
|
result(.failure(LightWalletServiceError.generalError(message: "error creating blockheight from BlockID \(blockID)")))
|
2020-04-09 15:25:43 -07:00
|
|
|
return
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
2020-04-09 15:25:43 -07:00
|
|
|
result(.success(blockHeight))
|
|
|
|
}
|
|
|
|
|
2021-04-21 13:02:23 -07:00
|
|
|
response.whenFailureBlocking(onto: queue) { error in
|
2020-08-10 15:19:59 -07:00
|
|
|
result(.failure(error.mapToServiceError()))
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func latestBlockHeightAsync() async throws -> BlockHeight {
|
|
|
|
try await BlockHeight(compactTxStreamerAsync.getLatestBlock(ChainSpec()).height)
|
|
|
|
}
|
|
|
|
|
2019-10-31 15:43:09 -07:00
|
|
|
public func blockRange(_ range: CompactBlockRange, result: @escaping (Result<[ZcashCompactBlock], LightWalletServiceError>) -> Void) {
|
2020-02-26 08:54:48 -08:00
|
|
|
queue.async { [weak self] in
|
|
|
|
guard let self = self else { return }
|
2021-09-17 06:49:58 -07:00
|
|
|
|
|
|
|
var blocks: [CompactBlock] = []
|
2020-04-09 15:25:43 -07:00
|
|
|
let response = self.compactTxStreamer.getBlockRange(range.blockRange(), handler: { blocks.append($0) })
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2019-10-18 11:45:19 -07:00
|
|
|
do {
|
2020-04-09 15:25:43 -07:00
|
|
|
let status = try response.status.wait()
|
|
|
|
switch status.code {
|
|
|
|
case .ok:
|
2021-06-03 07:51:12 -07:00
|
|
|
result(.success(blocks.asZcashCompactBlocks()))
|
|
|
|
|
2020-04-09 15:25:43 -07:00
|
|
|
default:
|
2020-08-10 15:19:59 -07:00
|
|
|
result(.failure(.mapCode(status)))
|
2020-04-09 15:25:43 -07:00
|
|
|
}
|
2019-10-18 11:45:19 -07:00
|
|
|
} catch {
|
2020-08-10 15:19:59 -07:00
|
|
|
result(.failure(error.mapToServiceError()))
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
|
|
|
|
let stream = compactTxStreamerAsync.getBlockRange(range.blockRange())
|
|
|
|
|
|
|
|
return AsyncThrowingStream { continuation in
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
for try await block in stream {
|
|
|
|
continuation.yield(ZcashCompactBlock(compactBlock: block))
|
|
|
|
}
|
|
|
|
continuation.finish(throwing: nil)
|
|
|
|
} catch {
|
2022-09-01 05:58:41 -07:00
|
|
|
continuation.finish(throwing: error.mapToServiceError())
|
2022-08-25 06:39:59 -07:00
|
|
|
}
|
|
|
|
}
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
|
|
|
}
|
2020-12-09 15:57:23 -08:00
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func submit(spendTransaction: Data, result: @escaping (Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void) {
|
2021-04-01 07:27:26 -07:00
|
|
|
do {
|
2022-08-25 06:39:59 -07:00
|
|
|
let transaction = try RawTransaction(serializedData: spendTransaction)
|
|
|
|
let response = self.compactTxStreamer.sendTransaction(transaction).response
|
|
|
|
|
|
|
|
response.whenComplete { responseResult in
|
|
|
|
switch responseResult {
|
|
|
|
case .failure(let error):
|
|
|
|
result(.failure(LightWalletServiceError.sentFailed(error: error)))
|
|
|
|
case .success(let success):
|
|
|
|
result(.success(success))
|
|
|
|
}
|
2021-04-01 07:27:26 -07:00
|
|
|
}
|
|
|
|
} catch {
|
2022-08-25 06:39:59 -07:00
|
|
|
result(.failure(error.mapToServiceError()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public func submitAsync(spendTransaction: Data) async throws -> LightWalletServiceResponse {
|
|
|
|
do {
|
|
|
|
let transaction = try RawTransaction(serializedData: spendTransaction)
|
|
|
|
return try await compactTxStreamerAsync.sendTransaction(transaction)
|
|
|
|
} catch {
|
|
|
|
throw LightWalletServiceError.sentFailed(error: error)
|
2021-04-01 07:27:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func fetchTransaction(txId: Data, result: @escaping (Result<TransactionEntity, LightWalletServiceError>) -> Void) {
|
|
|
|
var txFilter = TxFilter()
|
|
|
|
txFilter.hash = txId
|
|
|
|
|
|
|
|
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)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public func fetchTransactionAsync(txId: Data) async throws -> TransactionEntity {
|
|
|
|
var txFilter = TxFilter()
|
|
|
|
txFilter.hash = txId
|
|
|
|
|
|
|
|
let rawTx = try await compactTxStreamerAsync.getTransaction(txFilter)
|
|
|
|
return TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)
|
|
|
|
}
|
|
|
|
|
|
|
|
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]
|
2021-03-08 10:47:36 -08:00
|
|
|
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
|
|
|
|
2022-08-25 06:39:59 -07:00
|
|
|
public func fetchUTXOs(
|
|
|
|
for tAddress: String,
|
|
|
|
height: BlockHeight
|
|
|
|
) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
|
|
|
|
return fetchUTXOs(for: [tAddress], height: height)
|
2021-04-01 07:27:26 -07:00
|
|
|
}
|
2022-08-25 06:39:59 -07:00
|
|
|
|
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()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-25 06:39:59 -07:00
|
|
|
|
|
|
|
public func fetchUTXOs(
|
|
|
|
for tAddresses: [String],
|
|
|
|
height: BlockHeight
|
|
|
|
) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
|
|
|
|
guard !tAddresses.isEmpty else {
|
|
|
|
return AsyncThrowingStream { _ in }
|
|
|
|
}
|
|
|
|
|
|
|
|
let args = GetAddressUtxosArg.with { utxoArgs in
|
|
|
|
utxoArgs.addresses = tAddresses
|
|
|
|
utxoArgs.startHeight = UInt64(height)
|
|
|
|
}
|
|
|
|
let stream = compactTxStreamerAsync.getAddressUtxosStream(args)
|
|
|
|
|
|
|
|
return AsyncThrowingStream { continuation in
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
for try await reply in stream {
|
|
|
|
continuation.yield(
|
|
|
|
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
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
continuation.finish(throwing: nil)
|
|
|
|
} catch {
|
2022-09-01 05:58:41 -07:00
|
|
|
continuation.finish(throwing: error.mapToServiceError())
|
2022-08-25 06:39:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@discardableResult
|
|
|
|
public func blockStream(
|
|
|
|
startHeight: BlockHeight,
|
|
|
|
endHeight: BlockHeight,
|
|
|
|
result: @escaping (Result<GRPCResult, LightWalletServiceError>) -> Void,
|
|
|
|
handler: @escaping (ZcashCompactBlock) -> Void,
|
|
|
|
progress: @escaping (BlockProgress) -> Void
|
|
|
|
) -> CancellableCall {
|
|
|
|
let future = compactTxStreamer.getBlockRange(
|
|
|
|
BlockRange(
|
|
|
|
startHeight: startHeight,
|
|
|
|
endHeight: endHeight
|
|
|
|
),
|
|
|
|
callOptions: Self.callOptions(timeLimit: self.streamingCallTimeout),
|
|
|
|
handler: { compactBlock in
|
|
|
|
handler(ZcashCompactBlock(compactBlock: compactBlock))
|
|
|
|
progress(
|
|
|
|
BlockProgress(
|
|
|
|
startHeight: startHeight,
|
|
|
|
targetHeight: endHeight,
|
|
|
|
progressHeight: BlockHeight(compactBlock.height)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
future.status.whenComplete { completionResult in
|
|
|
|
switch completionResult {
|
|
|
|
case .success(let status):
|
|
|
|
switch status.code {
|
|
|
|
case .ok:
|
|
|
|
result(.success(GRPCResult.success))
|
|
|
|
default:
|
|
|
|
result(.failure(LightWalletServiceError.mapCode(status)))
|
|
|
|
}
|
|
|
|
case .failure(let error):
|
|
|
|
result(.failure(LightWalletServiceError.genericError(error: error)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return future
|
|
|
|
}
|
|
|
|
|
|
|
|
public func blockStream(
|
|
|
|
startHeight: BlockHeight,
|
|
|
|
endHeight: BlockHeight
|
|
|
|
) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
|
|
|
|
let stream = compactTxStreamerAsync.getBlockRange(
|
|
|
|
BlockRange(
|
|
|
|
startHeight: startHeight,
|
|
|
|
endHeight: endHeight
|
|
|
|
),
|
|
|
|
callOptions: Self.callOptions(timeLimit: self.streamingCallTimeout)
|
|
|
|
)
|
|
|
|
|
|
|
|
return AsyncThrowingStream { continuation in
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
for try await compactBlock in stream {
|
|
|
|
continuation.yield(ZcashCompactBlock(compactBlock: compactBlock))
|
|
|
|
}
|
|
|
|
continuation.finish(throwing: nil)
|
|
|
|
} catch {
|
2022-09-01 05:58:41 -07:00
|
|
|
continuation.finish(throwing: error.mapToServiceError())
|
2022-08-25 06:39:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension LightWalletGRPCService: LightWalletService {
|
|
|
|
public func closeConnection() {
|
|
|
|
_ = channel.close()
|
|
|
|
}
|
2020-08-10 15:19:59 -07:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-10 15:19:59 -07:00
|
|
|
extension Error {
|
|
|
|
func mapToServiceError() -> LightWalletServiceError {
|
2021-09-17 06:49:58 -07:00
|
|
|
guard let grpcError = self as? GRPCStatusTransformable else {
|
2020-08-10 15:19:59 -07:00
|
|
|
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
|
2020-08-10 15:19:59 -07:00
|
|
|
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")
|
2020-08-10 15:19:59 -07:00
|
|
|
case .deadlineExceeded:
|
|
|
|
return LightWalletServiceError.timeOut
|
|
|
|
default:
|
|
|
|
return LightWalletServiceError.genericError(error: status)
|
|
|
|
}
|
|
|
|
}
|
2019-10-18 11:45:19 -07:00
|
|
|
}
|
2021-04-20 15:30:59 -07:00
|
|
|
|
|
|
|
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
|
|
|
|
]
|
|
|
|
)
|
2021-04-20 15:30:59 -07:00
|
|
|
}
|
|
|
|
}
|