129 lines
5.5 KiB
Swift
129 lines
5.5 KiB
Swift
//
|
|
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import LibMobileCoin
|
|
|
|
struct TransactionStatusChecker {
|
|
private let account: ReadWriteDispatchLock<Account>
|
|
private let fogUntrustedTxOutFetcher: FogUntrustedTxOutFetcher
|
|
private let fogKeyImageChecker: FogKeyImageChecker
|
|
|
|
init(
|
|
account: ReadWriteDispatchLock<Account>,
|
|
fogUntrustedTxOutService: FogUntrustedTxOutService,
|
|
fogKeyImageService: FogKeyImageService,
|
|
targetQueue: DispatchQueue?
|
|
) {
|
|
self.account = account
|
|
self.fogUntrustedTxOutFetcher =
|
|
FogUntrustedTxOutFetcher(fogUntrustedTxOutService: fogUntrustedTxOutService)
|
|
self.fogKeyImageChecker =
|
|
FogKeyImageChecker(fogKeyImageService: fogKeyImageService, targetQueue: targetQueue)
|
|
}
|
|
|
|
func checkStatus(
|
|
_ transaction: Transaction,
|
|
completion: @escaping (Result<TransactionStatus, ConnectionError>) -> Void
|
|
) {
|
|
logger.info(
|
|
"Checking transaction status... transaction: " +
|
|
"\(redacting: transaction.serializedData.base64EncodedString())",
|
|
logFunction: false)
|
|
checkAcceptedStatus(transaction) {
|
|
completion($0.map {
|
|
let status = TransactionStatus($0)
|
|
switch status {
|
|
case .accepted(block: let block):
|
|
// Make sure we only return success if it will also be reflected in the balance,
|
|
// otherwise, feign ignorance.
|
|
guard block.index < self.account.readSync({ $0.knowableBlockCount }) else {
|
|
return .unknown
|
|
}
|
|
return status
|
|
case .unknown, .failed:
|
|
return status
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func checkAcceptedStatus(
|
|
_ transaction: Transaction,
|
|
completion: @escaping (Result<Transaction.AcceptedStatus, ConnectionError>) -> Void
|
|
) {
|
|
performAsync(body1: { callback in
|
|
fogUntrustedTxOutFetcher.getTxOut(
|
|
outputPublicKey: transaction.anyOutput.publicKey,
|
|
completion: callback)
|
|
}, body2: { callback in
|
|
fogKeyImageChecker.checkKeyImage(
|
|
keyImage: transaction.anyInputKeyImage,
|
|
completion: callback)
|
|
}, completion: {
|
|
completion($0.flatMap {
|
|
Self.acceptedStatus(
|
|
of: transaction,
|
|
keyImageSpentStatus: $0.1,
|
|
outputResult: $0.0.result,
|
|
outputBlockCount: $0.0.blockCount)
|
|
})
|
|
})
|
|
}
|
|
|
|
private static func acceptedStatus(
|
|
of transaction: Transaction,
|
|
keyImageSpentStatus: KeyImage.SpentStatus,
|
|
outputResult: FogLedger_TxOutResult,
|
|
outputBlockCount: UInt64
|
|
) -> Result<Transaction.AcceptedStatus, ConnectionError> {
|
|
// We assume the output public key is unique, therefore checking the existence of the output
|
|
// is enough to confirm Tx was accepted. However, at the moment we still need the key image
|
|
// check in order to get the block in which the Tx was accepted.
|
|
switch outputResult.resultCode {
|
|
case .found:
|
|
switch keyImageSpentStatus {
|
|
case .spent(block: let block):
|
|
return .success(.accepted(block: block))
|
|
case .unspent(knownToBeUnspentBlockCount: let knownToBeUnspentBlockCount):
|
|
// Output exists but key image server hasn't received it yet. At the moment, this
|
|
// means we must return .notAccepted since we don't know which block it was accepted
|
|
// in.
|
|
return .success(
|
|
.notAccepted(knownToBeNotAcceptedTotalBlockCount: knownToBeUnspentBlockCount))
|
|
}
|
|
case .notFound:
|
|
if outputBlockCount >= transaction.tombstoneBlockIndex {
|
|
return .success(.tombstoneBlockExceeded)
|
|
}
|
|
// Without confirming the output exists in the ledger, we can't be certain that the key
|
|
// image wasn't spent by another Tx (possibly by a different client using the same
|
|
// account key).
|
|
switch keyImageSpentStatus {
|
|
case .spent(block: let block):
|
|
if outputBlockCount > block.index {
|
|
// The output doexn't exist at the block height where the input was spent, so we
|
|
// know that the Tx failed.
|
|
return .success(.inputSpent)
|
|
} else {
|
|
// The input was spent, but we don't yet know whether it was spent by this Tx or
|
|
// not.
|
|
return .success(
|
|
.notAccepted(knownToBeNotAcceptedTotalBlockCount: outputBlockCount))
|
|
}
|
|
case .unspent(knownToBeUnspentBlockCount: let knownToBeUnspentBlockCount):
|
|
if knownToBeUnspentBlockCount >= transaction.tombstoneBlockIndex {
|
|
return .success(.tombstoneBlockExceeded)
|
|
} else {
|
|
return .success(.notAccepted(knownToBeNotAcceptedTotalBlockCount:
|
|
max(outputBlockCount, knownToBeUnspentBlockCount)))
|
|
}
|
|
}
|
|
case .malformedRequest, .databaseError, .UNRECOGNIZED:
|
|
return .failure(.invalidServerResponse("Fog UntrustedTxOut result error: " +
|
|
"\(outputResult.resultCode), response: \(outputResult)"))
|
|
}
|
|
}
|
|
}
|