# Title `v1.2.0-pre0` - Network Robustness, Fog 1.2.0, Apple Silicon/M1 & Mac Catalyst # Description Added a way for implementing apps to pass in there own `HttpRequester` which can be useful for network robustness. Subspec `LibMobileCoin` now supports Apple Silicon/M1 and Mac Catalyst. Code changes to support Fog v1.2.0. #### `pre1` Changes - Uses the `v1.2.0-pre1` version of `LibMobileCoin` which supports a higher version of `GRPC`. - Added bridging headers - Updated Gemfile/Makefile to use the latest `cocoapods` version `1.11.2` #### Future Work Fix the `docs` steps in the circleci build process. Will require `jazzy` related fixes and testing on Xcode 11. # Changes ## Network Robustness Adds a separate `HTTP` networking architecture. An object conforming to `protocol HttpRequester` can be provided to the `NetworkConfig` object when the `MobileCoinClient` is created. Then the `TransportProtocolOption` can be changed from `grpc` to `http`. > NOTE: This branch will not run because it depends on changes in other submodules that have not yet been merged. ### `HttpRequester` Implementing apps/frameworks should provide an object conforming to this protocol. Our `RestApiRequester` wraps around an `HttpRequester` to communicate with with our services using `protobuf`s. ``` Sources/Network/HttpConnection/HttpRequester.swift ``` ### Attested & Auth Connection Wrappers `HTTP` versions of the Auth & Attested wrapper classes. These protocol and their default implementations handle the logic paths needed for authentication/attestation and re-authentication/attestation: > Can become generic ``` Sources/Network/HttpConnection/ArbitraryHttpConnection.swift Sources/Network/HttpConnection/AttestedHttpConnection.swift ``` ### Networking Protocols Separate code-paths for `HTTP` versions of the networking protocols: ``` Sources/Network/HttpConnection/HttpCallable/AttestedHttpCallable.swift Sources/Network/HttpConnection/HttpCallable/AuthHttpCallable.swift Sources/Network/HttpConnection/HttpCallable/AuthHttpCallableClientWrapper.swift Sources/Network/HttpConnection/HttpCallable/HttpCallable.swift ``` `HTTP` "interfaces" that closely mimic functionality from `GRPC`. Allows us to re-use more of our existing patterns. ``` Sources/Network/HttpConnection/HTTPInterface/HTTPCallOptions.swift Sources/Network/HttpConnection/HTTPInterface/HTTPClient.swift Sources/Network/HttpConnection/HTTPInterface/HTTPClientCall.swift Sources/Network/HttpConnection/HTTPInterface/HTTPMethod.swift Sources/Network/HttpConnection/HTTPInterface/HTTPResponse.swift Sources/Network/HttpConnection/HTTPInterface/HTTPStatus.swift Sources/Network/HttpConnection/HTTPInterface/HTTPUnaryCall.swift ``` ### `HTTP` Connection Implementations Wrapper classes that interface directly with `protoc-swift` generated `.swift` files for our protobuf models. ``` Sources/Network/HttpConnection/HttpConnection.swift Sources/Network/HttpConnection/HttpConnections/BlockchainHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/ConsensusHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogBlockHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogKeyImageHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogMerkleProofHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogReportHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogUntrustedTxOutHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogViewHttpConnection.swift ``` ### `HTTP` versions of `protoc-swift` generated models The GRPC versions of these are generated by `protoc-swift`. The HTTP versions were edited by hand to work with the `HTTP` Connections implementations. > **This could be automated preferably with a `protoc-swift` plugin template but also `sed`/`VIM` if needed.** ``` Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/attest.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/consensus_client.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/consensus_common.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/ledger.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/report.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/view.http.swift ``` ## Fog Updates The latest version of `fog` changes the name of a protobuf `FogLedger_Block` -> `FogLedger_BlockData`. ### Compressed Commitment The `TxOut` compressed commitment is no longer sent in the protobuf message because it can be reconstructed with its constituent parts (+ the user's `view_private_key`) We now reconstruct the commitment in several places whereas before it was being returned from the decoded protobuf message. Lastly some function signatures into `LibMobileCoin` were updated to adjust to the changes. ## Miscellaneous New MrEnclave values Support for Apple Silicon/M1 & Mac Catalyst ## Unit Tests One unit test was removed. It creates a TxOut and tries to unmask the value with an **incorrect** private view key. The return value should be 'nil' but is noise. It will require a change in the rust code and should be implemented in a future release. Some objects were re-serialized to match the new TxOut structure. Credentials are not required for `consensus` so this has been changed in the `NetworkConfig` Tests can be run with `TransportProtocol == .http` by changing the default value in `NetworkConfig`
402 lines
16 KiB
Swift
402 lines
16 KiB
Swift
//
|
|
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
|
|
//
|
|
|
|
// swiftlint:disable function_parameter_count multiline_arguments multiline_function_chains
|
|
|
|
import Foundation
|
|
import NIOSSL
|
|
|
|
public final class MobileCoinClient {
|
|
/// - Returns: `InvalidInputError` when `accountKey` isn't configured to use Fog.
|
|
public static func make(accountKey: AccountKey, config: Config)
|
|
-> Result<MobileCoinClient, InvalidInputError>
|
|
{
|
|
guard let accountKey = AccountKeyWithFog(accountKey: accountKey) else {
|
|
let errorMessage = "Accounts without fog URLs are not currently supported."
|
|
logger.error(errorMessage, logFunction: false)
|
|
return .failure(InvalidInputError(errorMessage))
|
|
}
|
|
|
|
return .success(MobileCoinClient(accountKey: accountKey, config: config))
|
|
}
|
|
|
|
private let accountLock: ReadWriteDispatchLock<Account>
|
|
private let serialQueue: DispatchQueue
|
|
private let callbackQueue: DispatchQueue
|
|
|
|
private let txOutSelectionStrategy: TxOutSelectionStrategy
|
|
private let mixinSelectionStrategy: MixinSelectionStrategy
|
|
private let fogQueryScalingStrategy: FogQueryScalingStrategy
|
|
|
|
private let serviceProvider: ServiceProvider
|
|
private let fogResolverManager: FogResolverManager
|
|
private let feeFetcher: BlockchainFeeFetcher
|
|
|
|
init(accountKey: AccountKeyWithFog, config: Config) {
|
|
logger.info("""
|
|
Initializing \(Self.self):
|
|
\(Self.configDescription(accountKey: accountKey, config: config))
|
|
""", logFunction: false)
|
|
|
|
self.serialQueue = DispatchQueue(label: "com.mobilecoin.\(Self.self)")
|
|
self.callbackQueue = config.callbackQueue ?? DispatchQueue.main
|
|
self.accountLock = .init(Account(accountKey: accountKey))
|
|
self.txOutSelectionStrategy = config.txOutSelectionStrategy
|
|
self.mixinSelectionStrategy = config.mixinSelectionStrategy
|
|
self.fogQueryScalingStrategy = config.fogQueryScalingStrategy
|
|
|
|
self.serviceProvider =
|
|
DefaultServiceProvider(networkConfig: config.networkConfig, targetQueue: serialQueue)
|
|
self.fogResolverManager = FogResolverManager(
|
|
fogReportAttestation: config.networkConfig.fogReportAttestation,
|
|
serviceProvider: serviceProvider,
|
|
targetQueue: serialQueue)
|
|
self.feeFetcher = BlockchainFeeFetcher(
|
|
blockchainService: serviceProvider.blockchainService,
|
|
minimumFeeCacheTTL: config.minimumFeeCacheTTL,
|
|
targetQueue: serialQueue)
|
|
}
|
|
|
|
public var balance: Balance {
|
|
accountLock.readSync { $0.cachedBalance }
|
|
}
|
|
|
|
public var accountActivity: AccountActivity {
|
|
accountLock.readSync { $0.cachedAccountActivity }
|
|
}
|
|
|
|
public func setTransportProtocol(_ transportProtocol: TransportProtocol) {
|
|
serviceProvider.setTransportProtocolOption(transportProtocol.option)
|
|
}
|
|
|
|
public func setConsensusBasicAuthorization(username: String, password: String) {
|
|
let credentials = BasicCredentials(username: username, password: password)
|
|
serviceProvider.setConsensusAuthorization(credentials: credentials)
|
|
}
|
|
|
|
public func setFogBasicAuthorization(username: String, password: String) {
|
|
let credentials = BasicCredentials(username: username, password: password)
|
|
serviceProvider.setFogUserAuthorization(credentials: credentials)
|
|
}
|
|
|
|
public func updateBalance(completion: @escaping (Result<Balance, ConnectionError>) -> Void) {
|
|
Account.BalanceUpdater(
|
|
account: accountLock,
|
|
fogViewService: serviceProvider.fogViewService,
|
|
fogKeyImageService: serviceProvider.fogKeyImageService,
|
|
fogBlockService: serviceProvider.fogBlockService,
|
|
fogQueryScalingStrategy: fogQueryScalingStrategy,
|
|
targetQueue: serialQueue
|
|
).updateBalance { result in
|
|
self.callbackQueue.async {
|
|
completion(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func amountTransferable(
|
|
feeLevel: FeeLevel = .minimum,
|
|
completion: @escaping (Result<UInt64, BalanceTransferEstimationFetcherError>) -> Void
|
|
) {
|
|
Account.TransactionEstimator(
|
|
account: accountLock,
|
|
feeFetcher: feeFetcher,
|
|
txOutSelectionStrategy: txOutSelectionStrategy,
|
|
targetQueue: serialQueue
|
|
).amountTransferable(feeLevel: feeLevel, completion: completion)
|
|
}
|
|
|
|
public func estimateTotalFee(
|
|
toSendAmount amount: UInt64,
|
|
feeLevel: FeeLevel = .minimum,
|
|
completion: @escaping (Result<UInt64, TransactionEstimationFetcherError>) -> Void
|
|
) {
|
|
Account.TransactionEstimator(
|
|
account: accountLock,
|
|
feeFetcher: feeFetcher,
|
|
txOutSelectionStrategy: txOutSelectionStrategy,
|
|
targetQueue: serialQueue
|
|
).estimateTotalFee(toSendAmount: amount, feeLevel: feeLevel, completion: completion)
|
|
}
|
|
|
|
public func requiresDefragmentation(
|
|
toSendAmount amount: UInt64,
|
|
feeLevel: FeeLevel = .minimum,
|
|
completion: @escaping (Result<Bool, TransactionEstimationFetcherError>) -> Void
|
|
) {
|
|
Account.TransactionEstimator(
|
|
account: accountLock,
|
|
feeFetcher: feeFetcher,
|
|
txOutSelectionStrategy: txOutSelectionStrategy,
|
|
targetQueue: serialQueue
|
|
).requiresDefragmentation(toSendAmount: amount, feeLevel: feeLevel, completion: completion)
|
|
}
|
|
|
|
public func prepareTransaction(
|
|
to recipient: PublicAddress,
|
|
amount: UInt64,
|
|
fee: UInt64,
|
|
completion: @escaping (
|
|
Result<(transaction: Transaction, receipt: Receipt), TransactionPreparationError>
|
|
) -> Void
|
|
) {
|
|
Account.TransactionOperations(
|
|
account: accountLock,
|
|
fogMerkleProofService: serviceProvider.fogMerkleProofService,
|
|
fogResolverManager: fogResolverManager,
|
|
feeFetcher: feeFetcher,
|
|
txOutSelectionStrategy: txOutSelectionStrategy,
|
|
mixinSelectionStrategy: mixinSelectionStrategy,
|
|
targetQueue: serialQueue
|
|
).prepareTransaction(to: recipient, amount: amount, fee: fee) { result in
|
|
self.callbackQueue.async {
|
|
completion(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func prepareTransaction(
|
|
to recipient: PublicAddress,
|
|
amount: UInt64,
|
|
feeLevel: FeeLevel = .minimum,
|
|
completion: @escaping (
|
|
Result<(transaction: Transaction, receipt: Receipt), TransactionPreparationError>
|
|
) -> Void
|
|
) {
|
|
Account.TransactionOperations(
|
|
account: accountLock,
|
|
fogMerkleProofService: serviceProvider.fogMerkleProofService,
|
|
fogResolverManager: fogResolverManager,
|
|
feeFetcher: feeFetcher,
|
|
txOutSelectionStrategy: txOutSelectionStrategy,
|
|
mixinSelectionStrategy: mixinSelectionStrategy,
|
|
targetQueue: serialQueue
|
|
).prepareTransaction(to: recipient, amount: amount, feeLevel: feeLevel) { result in
|
|
self.callbackQueue.async {
|
|
completion(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func prepareDefragmentationStepTransactions(
|
|
toSendAmount amount: UInt64,
|
|
feeLevel: FeeLevel = .minimum,
|
|
completion: @escaping (Result<[Transaction], DefragTransactionPreparationError>) -> Void
|
|
) {
|
|
Account.TransactionOperations(
|
|
account: accountLock,
|
|
fogMerkleProofService: serviceProvider.fogMerkleProofService,
|
|
fogResolverManager: fogResolverManager,
|
|
feeFetcher: feeFetcher,
|
|
txOutSelectionStrategy: txOutSelectionStrategy,
|
|
mixinSelectionStrategy: mixinSelectionStrategy,
|
|
targetQueue: serialQueue
|
|
).prepareDefragmentationStepTransactions(toSendAmount: amount, feeLevel: feeLevel)
|
|
{ result in
|
|
self.callbackQueue.async {
|
|
completion(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func submitTransaction(
|
|
_ transaction: Transaction,
|
|
completion: @escaping (Result<(), TransactionSubmissionError>) -> Void
|
|
) {
|
|
TransactionSubmitter(
|
|
consensusService: serviceProvider.consensusService,
|
|
feeFetcher: feeFetcher
|
|
).submitTransaction(transaction) { result in
|
|
self.callbackQueue.async {
|
|
completion(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func status(
|
|
of transaction: Transaction,
|
|
completion: @escaping (Result<TransactionStatus, ConnectionError>) -> Void
|
|
) {
|
|
TransactionStatusChecker(
|
|
account: accountLock,
|
|
fogUntrustedTxOutService: serviceProvider.fogUntrustedTxOutService,
|
|
fogKeyImageService: serviceProvider.fogKeyImageService,
|
|
targetQueue: serialQueue
|
|
).checkStatus(transaction) { result in
|
|
self.callbackQueue.async {
|
|
completion(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func status(of receipt: Receipt) -> Result<ReceiptStatus, InvalidInputError> {
|
|
ReceiptStatusChecker(account: accountLock).status(receipt)
|
|
}
|
|
}
|
|
|
|
extension MobileCoinClient {
|
|
private static func configDescription(accountKey: AccountKeyWithFog, config: Config) -> String {
|
|
let fogInfo = accountKey.fogInfo
|
|
return """
|
|
Consensus url: \(config.networkConfig.consensusUrl.url)
|
|
Fog url: \(config.networkConfig.fogUrl.url)
|
|
AccountKey PublicAddress: \
|
|
\(redacting: Base58Coder.encode(accountKey.accountKey.publicAddress))
|
|
AccountKey Fog Report url: \(fogInfo.reportUrl.url)
|
|
AccountKey Fog Report id: \(String(reflecting: fogInfo.reportId))
|
|
AccountKey Fog Report authority sPKI: 0x\(fogInfo.authoritySpki.hexEncodedString())
|
|
Consensus attestation: \(config.networkConfig.consensus.attestation)
|
|
Fog View attestation: \(config.networkConfig.fogView.attestation)
|
|
Fog KeyImage attestation: \(config.networkConfig.fogKeyImage.attestation)
|
|
Fog MerkleProof attestation: \(config.networkConfig.fogMerkleProof.attestation)
|
|
Fog Report attestation: \(config.networkConfig.fogReportAttestation)
|
|
"""
|
|
}
|
|
}
|
|
|
|
extension MobileCoinClient {
|
|
@available(*, deprecated, message: "Use amountTransferable(feeLevel:completion:) instead")
|
|
public func amountTransferable(feeLevel: FeeLevel = .minimum)
|
|
-> Result<UInt64, BalanceTransferEstimationError>
|
|
{
|
|
Account.TransactionEstimator(
|
|
account: accountLock,
|
|
feeFetcher: feeFetcher,
|
|
txOutSelectionStrategy: txOutSelectionStrategy,
|
|
targetQueue: serialQueue
|
|
).amountTransferable(feeLevel: feeLevel)
|
|
}
|
|
|
|
@available(*, deprecated, message:
|
|
"Use estimateTotalFee(toSendAmount:feeLevel:completion:) instead")
|
|
public func estimateTotalFee(
|
|
toSendAmount amount: UInt64,
|
|
feeLevel: FeeLevel = .minimum
|
|
) -> Result<UInt64, TransactionEstimationError> {
|
|
Account.TransactionEstimator(
|
|
account: accountLock,
|
|
feeFetcher: feeFetcher,
|
|
txOutSelectionStrategy: txOutSelectionStrategy,
|
|
targetQueue: serialQueue
|
|
).estimateTotalFee(toSendAmount: amount, feeLevel: feeLevel)
|
|
}
|
|
|
|
@available(*, deprecated, message:
|
|
"Use requiresDefragmentation(toSendAmount:feeLevel:completion:) instead")
|
|
public func requiresDefragmentation(toSendAmount amount: UInt64, feeLevel: FeeLevel = .minimum)
|
|
-> Result<Bool, TransactionEstimationError>
|
|
{
|
|
Account.TransactionEstimator(
|
|
account: accountLock,
|
|
feeFetcher: feeFetcher,
|
|
txOutSelectionStrategy: txOutSelectionStrategy,
|
|
targetQueue: serialQueue
|
|
).requiresDefragmentation(toSendAmount: amount, feeLevel: feeLevel)
|
|
}
|
|
}
|
|
|
|
extension MobileCoinClient {
|
|
public struct Config {
|
|
/// - Returns: `InvalidInputError` when `consensusUrl` or `fogUrl` are not well-formed URLs
|
|
/// with the appropriate schemes.
|
|
public static func make(
|
|
consensusUrl: String,
|
|
consensusAttestation: Attestation,
|
|
fogUrl: String,
|
|
fogViewAttestation: Attestation,
|
|
fogKeyImageAttestation: Attestation,
|
|
fogMerkleProofAttestation: Attestation,
|
|
fogReportAttestation: Attestation
|
|
) -> Result<Config, InvalidInputError> {
|
|
ConsensusUrl.make(string: consensusUrl).flatMap { consensusUrl in
|
|
FogUrl.make(string: fogUrl).map { fogUrl in
|
|
let attestationConfig = NetworkConfig.AttestationConfig(
|
|
consensus: consensusAttestation,
|
|
fogView: fogViewAttestation,
|
|
fogKeyImage: fogKeyImageAttestation,
|
|
fogMerkleProof: fogMerkleProofAttestation,
|
|
fogReport: fogReportAttestation)
|
|
let networkConfig = NetworkConfig(
|
|
consensusUrl: consensusUrl,
|
|
fogUrl: fogUrl,
|
|
attestation: attestationConfig)
|
|
return Config(networkConfig: networkConfig)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate var networkConfig: NetworkConfig
|
|
|
|
// default minimum fee cache TTL is 30 minutes
|
|
public var minimumFeeCacheTTL: TimeInterval = 30 * 60
|
|
|
|
public var cacheStorageAdapter: StorageAdapter?
|
|
|
|
/// The `DispatchQueue` on which all `MobileCoinClient` completion handlers will be called.
|
|
/// If `nil`, `DispatchQueue.main` will be used.
|
|
public var callbackQueue: DispatchQueue?
|
|
|
|
var txOutSelectionStrategy: TxOutSelectionStrategy = DefaultTxOutSelectionStrategy()
|
|
var mixinSelectionStrategy: MixinSelectionStrategy = DefaultMixinSelectionStrategy()
|
|
var fogQueryScalingStrategy: FogQueryScalingStrategy = DefaultFogQueryScalingStrategy()
|
|
|
|
init(networkConfig: NetworkConfig) {
|
|
self.networkConfig = networkConfig
|
|
}
|
|
|
|
public var transportProtocol: TransportProtocol {
|
|
get { networkConfig.transportProtocol }
|
|
set { networkConfig.transportProtocol = newValue }
|
|
}
|
|
|
|
public mutating func setConsensusTrustRoots(_ trustRoots: [Data])
|
|
-> Result<(), InvalidInputError>
|
|
{
|
|
Self.parseTrustRoots(trustRootsBytes: trustRoots).map {
|
|
networkConfig.consensusTrustRoots = $0
|
|
}
|
|
}
|
|
|
|
public mutating func setFogTrustRoots(_ trustRoots: [Data]) -> Result<(), InvalidInputError>
|
|
{
|
|
Self.parseTrustRoots(trustRootsBytes: trustRoots).map {
|
|
networkConfig.fogTrustRoots = $0
|
|
}
|
|
}
|
|
|
|
public mutating func setConsensusBasicAuthorization(username: String, password: String) {
|
|
networkConfig.consensusAuthorization =
|
|
BasicCredentials(username: username, password: password)
|
|
}
|
|
|
|
public mutating func setFogBasicAuthorization(username: String, password: String) {
|
|
networkConfig.fogUserAuthorization =
|
|
BasicCredentials(username: username, password: password)
|
|
}
|
|
|
|
public var httpRequester: HttpRequester? {
|
|
get { networkConfig.httpRequester }
|
|
set { networkConfig.httpRequester = newValue }
|
|
}
|
|
|
|
private static func parseTrustRoots(trustRootsBytes: [Data])
|
|
-> Result<[NIOSSLCertificate], InvalidInputError>
|
|
{
|
|
var trustRoots: [NIOSSLCertificate] = []
|
|
for trustRootBytes in trustRootsBytes {
|
|
do {
|
|
trustRoots.append(
|
|
try NIOSSLCertificate(bytes: Array(trustRootBytes), format: .der))
|
|
} catch {
|
|
let errorMessage = "Error parsing trust root certificate: " +
|
|
"\(trustRootBytes.base64EncodedString()) - Error: \(error)"
|
|
logger.error(errorMessage, logFunction: false)
|
|
return .failure(InvalidInputError(errorMessage))
|
|
}
|
|
}
|
|
return .success(trustRoots)
|
|
}
|
|
}
|
|
}
|