diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIPayments.swift b/Signal/src/ViewControllers/DebugUI/DebugUIPayments.swift index 7762ecd45e..aac08a3d52 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIPayments.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIPayments.swift @@ -312,6 +312,8 @@ class DebugUIPayments: DebugUIPage { memoMessage: "Tiny: \(count)", paymentRequestModel: nil, isOutgoingTransfer: false) + }.then(on: .global()) { (paymentModel: TSPaymentModel) in + Self.payments.blockOnOutgoingVerification(paymentModel: paymentModel) }.done(on: .global()) { _ in if count > 1 { Self.sendTinyPayments(contactThread: contactThread, count: count - 1) diff --git a/Signal/src/ViewControllers/Payments/SendPaymentCompletionActionSheet.swift b/Signal/src/ViewControllers/Payments/SendPaymentCompletionActionSheet.swift index 459525bc73..5071e3034b 100644 --- a/Signal/src/ViewControllers/Payments/SendPaymentCompletionActionSheet.swift +++ b/Signal/src/ViewControllers/Payments/SendPaymentCompletionActionSheet.swift @@ -509,7 +509,7 @@ public class SendPaymentCompletionActionSheet: ActionSheetController { // TODO: Finalize this timeout duration with design. let blockInterval: TimeInterval = kSecondInterval * 30 return firstly(on: .global()) { () -> Promise in - Self.paymentsSwift.blockOnOutgoingVerification(paymentModel: paymentModel) + Self.paymentsSwift.blockOnOutgoingVerification(paymentModel: paymentModel).asVoid() }.timeout(seconds: blockInterval, description: "Payments Verify Submission") { PaymentsError.timeout }.recover(on: .global()) { (error: Error) -> Guarantee<()> in diff --git a/SignalMessaging/Payments/MobileCoinAPI.swift b/SignalMessaging/Payments/MobileCoinAPI.swift index cfc2371932..4e38589460 100644 --- a/SignalMessaging/Payments/MobileCoinAPI.swift +++ b/SignalMessaging/Payments/MobileCoinAPI.swift @@ -596,78 +596,6 @@ class MobileCoinAPI { } } - // private func defragmentIfNecessary(forPaymentAmount paymentAmount: TSPaymentAmount) -> Promise { - // Logger.verbose("") - // - // let client = self.client - // - // return firstly(on: .global()) { () throws -> Promise in - // let result = client.requiresDefragmentation(toSendAmount: paymentAmount.picoMob, feeLevel: Self.feeLevel) - // switch result { - // case .success(let shouldDefragment): - // guard shouldDefragment else { - // return Promise.value(()) - // } - // return self.defragment(forPaymentAmount: paymentAmount) - // case .failure(let error): - // let error = Self.convertMCError(error: error) - // throw error - // } - // } - // } - // - // private func defragment(forPaymentAmount paymentAmount: TSPaymentAmount) -> Promise { - // Logger.verbose("") - // - // let client = self.client - // - // return firstly(on: .global()) { () throws -> Promise<[MobileCoin.Transaction]> in - // let (promise, resolver) = Promise<[MobileCoin.Transaction]>.pending() - // if DebugFlags.paymentsNoRequestsComplete.get() { - // // Never resolve. - // return promise - // } - // client.prepareDefragmentationStepTransactions(toSendAmount: paymentAmount.picoMob, - // feeLevel: Self.feeLevel) { (result: Swift.Result<[MobileCoin.Transaction], - // MobileCoin.TransactionPreparationError>) in - // switch result { - // case .success(let transactions): - // resolver.fulfill(transactions) - // break - // case .failure(let error): - // let error = Self.convertMCError(error: error) - // resolver.reject(error) - // break - // } - // } - // return promise - // }.then(on: .global()) { (transactions: [MobileCoin.Transaction]) -> Promise in - // // 1. Prepare defragmentation transactions. - // // 2. Record defragmentation transactions in database. - // // 3. Notify linked devices of possible defragmentation transactions. - // // 4. Submit defragmentation transactions. - // // 5. Verify defragmentation transactions. - // return firstly(on: .global()) { () throws -> Promise in - // let submissionPromises = transactions.map { transaction in - // self.submitTransaction(transaction: transaction) - // } - // return when(fulfilled: submissionPromises) - // }.then(on: .global()) { () -> Promise in - // let submissionPromises = transactions.map { transaction in - // self.getOutgoingTransactionStatus(transaction: transaction) - // } - // return when(fulfilled: submissionPromises) - // }.then(on: .global()) { (transactionStatuses: [MCOutgoingTransactionStatus]) -> Promise in - // } - //// func getOutgoingTransactionStatus(transaction: MobileCoin.Transaction) -> Promise { - // } - // } - // public func prepareDefragmentationStepTransactions( - // toSendAmount amount: UInt64, - // feeLevel: FeeLevel = .minimum, - // completion: @escaping (Result<[Transaction], TransactionPreparationError>) -> Void - // ) { - func submitTransaction(transaction: MobileCoin.Transaction) -> Promise { Logger.verbose("") @@ -982,7 +910,8 @@ public extension PaymentsError { .missingModel, .defragmentationRequired, .invalidTransaction, - .inputsAlreadySpent: + .inputsAlreadySpent, + .defragmentationFailed: return false case .connectionFailure, .timeout: diff --git a/SignalMessaging/Payments/PaymentsImpl.swift b/SignalMessaging/Payments/PaymentsImpl.swift index 37f8a8d75b..537e5543dd 100644 --- a/SignalMessaging/Payments/PaymentsImpl.swift +++ b/SignalMessaging/Payments/PaymentsImpl.swift @@ -478,27 +478,27 @@ public class PaymentsImpl: NSObject, PaymentsSwift { return [ // iPhone 11 Pro Max Simulator try! DevDevice(phoneNumber: "+441752395464", - fakeRootEntropy: MobileCoinAPI.rootEntropy1), + fakeRootEntropy: MobileCoinAPI.rootEntropy1) - // iPhone Xs Simulator - try! DevDevice(phoneNumber: "+14503002620", - fakeRootEntropy: MobileCoinAPI.rootEntropy2), - - // iPhone Xr Simulator - try! DevDevice(phoneNumber: "+13602090656", - fakeRootEntropy: MobileCoinAPI.rootEntropy3), - - // iPhone XS Max Device - try! DevDevice(phoneNumber: "+12262864592", - fakeRootEntropy: MobileCoinAPI.rootEntropy4), - - // iPhone 12 mini Device - try! DevDevice(phoneNumber: "+12534877762", - fakeRootEntropy: MobileCoinAPI.rootEntropy5), - - // iPhone 11 Pro Simulator - try! DevDevice(phoneNumber: "+15092608677", - fakeRootEntropy: MobileCoinAPI.rootEntropy6) +// // iPhone Xs Simulator +// try! DevDevice(phoneNumber: "+14503002620", +// fakeRootEntropy: MobileCoinAPI.rootEntropy2), +// +// // iPhone Xr Simulator +// try! DevDevice(phoneNumber: "+13602090656", +// fakeRootEntropy: MobileCoinAPI.rootEntropy3), +// +// // iPhone XS Max Device +// try! DevDevice(phoneNumber: "+12262864592", +// fakeRootEntropy: MobileCoinAPI.rootEntropy4), +// +// // iPhone 12 mini Device +// try! DevDevice(phoneNumber: "+12534877762", +// fakeRootEntropy: MobileCoinAPI.rootEntropy5), +// +// // iPhone 11 Pro Simulator +// try! DevDevice(phoneNumber: "+15092608677", +// fakeRootEntropy: MobileCoinAPI.rootEntropy6) ] } @@ -935,27 +935,6 @@ public extension PaymentsImpl { // 6. Block defragmentation transactions. return firstly(on: .global()) { () throws -> Promise<[MobileCoin.Transaction]> in mobileCoinAPI.prepareDefragmentationStepTransactions(forPaymentAmount: paymentAmount) - // { - // - // let (promise, resolver) = Promise<[MobileCoin.Transaction]>.pending() - // if DebugFlags.paymentsNoRequestsComplete.get() { - // // Never resolve. - // return promise - // } - // client.prepareDefragmentationStepTransactions(toSendAmount: paymentAmount.picoMob, - // feeLevel: Self.feeLevel) { (result: Swift.Result<[MobileCoin.Transaction], - // MobileCoin.TransactionPreparationError>) in - // switch result { - // case .success(let transactions): - // resolver.fulfill(transactions) - // break - // case .failure(let error): - // let error = Self.convertMCError(error: error) - // resolver.reject(error) - // break - // } - // } - // return promise }.map(on: .global()) { (mcTransactions: [MobileCoin.Transaction]) -> [TSPaymentModel] in // To initiate the defragmentation transactions, all we need to do // is save TSPaymentModels to the database. The PaymentsProcessor @@ -1009,7 +988,13 @@ public extension PaymentsImpl { return firstly(on: .global()) { () -> Promise in let promises = paymentModels.map { paymentModel in - self.blockOnOutgoingVerification(paymentModel: paymentModel) + firstly(on: .global()) { () -> Promise in + self.blockOnOutgoingVerification(paymentModel: paymentModel) + }.map(on: .global()) { (didSucceed: Bool) -> Void in + guard didSucceed else { + throw PaymentsError.defragmentationFailed + } + } } return when(fulfilled: promises) }.timeout(seconds: maxBlockInterval, description: "blockOnVerificationOfDefragmentation") { () -> Error in @@ -1017,8 +1002,8 @@ public extension PaymentsImpl { } } - func blockOnOutgoingVerification(paymentModel: TSPaymentModel) -> Promise { - firstly(on: .global()) { () -> Promise in + func blockOnOutgoingVerification(paymentModel: TSPaymentModel) -> Promise { + firstly(on: .global()) { () -> Promise in let paymentModelLatest = Self.databaseStorage.read { transaction in TSPaymentModel.anyFetch(uniqueId: paymentModel.uniqueId, transaction: transaction) @@ -1032,7 +1017,7 @@ public extension PaymentsImpl { // Not yet verified, wait then try again. return firstly(on: .global()) { after(seconds: 0.05) - }.then(on: .global()) { () -> Promise in + }.then(on: .global()) { () -> Promise in // Recurse. self.blockOnOutgoingVerification(paymentModel: paymentModel) } @@ -1040,10 +1025,12 @@ public extension PaymentsImpl { .outgoingSending, .outgoingSent, .outgoingMissingLedgerTimestamp, - .outgoingComplete, - .outgoingFailed: - // Success: Verified or failed. - return Promise.value(()) + .outgoingComplete: + // Success: Verified. + return Promise.value(true) + case .outgoingFailed: + // Success: Failed. + return Promise.value(false) case .incomingUnverified, .incomingVerified, .incomingMissingLedgerTimestamp, diff --git a/SignalMessaging/Payments/PaymentsProcessor.swift b/SignalMessaging/Payments/PaymentsProcessor.swift index 066b1a5149..010efcb9de 100644 --- a/SignalMessaging/Payments/PaymentsProcessor.swift +++ b/SignalMessaging/Payments/PaymentsProcessor.swift @@ -542,7 +542,8 @@ private class PaymentProcessingOperation: OWSOperation { .connectionFailure, .timeout, .invalidTransaction, - .inputsAlreadySpent: + .inputsAlreadySpent, + .defragmentationFailed: owsFailDebugUnlessMCNetworkFailure(error) case .authorizationFailure: owsFailDebugUnlessMCNetworkFailure(error) @@ -595,7 +596,8 @@ private class PaymentProcessingOperation: OWSOperation { .serializationError, .missingModel, .invalidTransaction, - .inputsAlreadySpent: + .inputsAlreadySpent, + .defragmentationFailed: // Do not retry these errors. delegate?.endProcessing(paymentId: self.paymentId) case .serverRateLimited: @@ -828,9 +830,10 @@ private class PaymentProcessingOperation: OWSOperation { // If we've verified a payment, our balance may have changed. Self.payments.updateCurrentPaymentBalance() case .failed: - try paymentModel.updatePaymentModelState(fromState: .outgoingUnverified, - toState: .outgoingFailed, - transaction: transaction) + Self.markAsFailed(paymentModel: paymentModel, + paymentFailure: .validationFailed, + paymentState: .outgoingFailed, + transaction: transaction) } } } diff --git a/SignalServiceKit/src/Payments/Payments.swift b/SignalServiceKit/src/Payments/Payments.swift index 66bff87be9..b340571150 100644 --- a/SignalServiceKit/src/Payments/Payments.swift +++ b/SignalServiceKit/src/Payments/Payments.swift @@ -33,6 +33,7 @@ public enum PaymentsError: Error { case defragmentationRequired case invalidTransaction case inputsAlreadySpent + case defragmentationFailed } // MARK: - @@ -150,7 +151,7 @@ public protocol PaymentsSwift: Payments { var allPossiblePassphraseWords: [String] { get } - func blockOnOutgoingVerification(paymentModel: TSPaymentModel) -> Promise + func blockOnOutgoingVerification(paymentModel: TSPaymentModel) -> Promise } // MARK: - @@ -476,7 +477,7 @@ extension MockPayments: PaymentsSwift { owsFail("Not implemented.") } - public func blockOnOutgoingVerification(paymentModel: TSPaymentModel) -> Promise { + public func blockOnOutgoingVerification(paymentModel: TSPaymentModel) -> Promise { owsFail("Not implemented.") } } diff --git a/SignalServiceKit/src/Payments/TSPaymentModels.swift b/SignalServiceKit/src/Payments/TSPaymentModels.swift index 9975b83a8e..fcf7bca193 100644 --- a/SignalServiceKit/src/Payments/TSPaymentModels.swift +++ b/SignalServiceKit/src/Payments/TSPaymentModels.swift @@ -437,12 +437,12 @@ extension TSPaymentModel: TSPaymentBaseModel { isValid = false } - let shouldHaveMCRecipientPublicAddressData = isOutgoing && (isIdentifiedPayment || isOutgoingTransfer) + let shouldHaveMCRecipientPublicAddressData = isOutgoing && (isIdentifiedPayment || isOutgoingTransfer) && !isFailed if shouldHaveMCRecipientPublicAddressData, mcRecipientPublicAddressData == nil { owsFailDebug("Missing mcRecipientPublicAddressData: \(formattedState).") } - let shouldHaveMCTransaction = isOutgoing && !isUnidentified + let shouldHaveMCTransaction = isOutgoing && !isUnidentified && !isFailed if shouldHaveMCTransaction, mcTransactionData == nil { owsFailDebug("Missing mcTransactionData: \(formattedState).") } @@ -471,7 +471,7 @@ extension TSPaymentModel: TSPaymentBaseModel { isValid = false } - let shouldHaveMCSpentKeyImages = isOutgoing + let shouldHaveMCSpentKeyImages = isOutgoing && !isFailed if shouldHaveMCSpentKeyImages, mcSpentKeyImages == nil { owsFailDebug("Missing mcSpentKeyImages: \(formattedState).") @@ -482,8 +482,8 @@ extension TSPaymentModel: TSPaymentBaseModel { isValid = false } - let canHaveMCOutputPublicKeys = isOutgoing - let shouldHaveMCOutputPublicKeys = isOutgoing && !isUnidentified + let canHaveMCOutputPublicKeys = isOutgoing && !isFailed + let shouldHaveMCOutputPublicKeys = isOutgoing && !isUnidentified && !isFailed if shouldHaveMCOutputPublicKeys, mcOutputPublicKeys == nil { owsFailDebug("Missing mcOutputPublicKeys: \(formattedState).") @@ -494,20 +494,31 @@ extension TSPaymentModel: TSPaymentBaseModel { isValid = false } - let shouldHaveMCLedgerBlockTimestamp = isComplete && !isUnidentified + let shouldHaveMCLedgerBlockTimestamp = isComplete && !isUnidentified && !isFailed if shouldHaveMCLedgerBlockTimestamp, !hasMCLedgerBlockTimestamp { // For some payments, we'll never be able to fill in the block timestamp. Logger.warn("Missing mcLedgerBlockTimestamp: \(formattedState).") } - let shouldHaveMCLedgerBlockIndex = isVerified || isUnidentified + let shouldHaveMCLedgerBlockIndex = isVerified || isUnidentified && !isFailed if shouldHaveMCLedgerBlockIndex, !hasMCLedgerBlockIndex { owsFailDebug("Missing mcLedgerBlockIndex: \(formattedState).") isValid = false } + let shouldHaveMobileCoin = !isFailed + if shouldHaveMobileCoin, + mobileCoin == nil { + owsFailDebug("Missing mobileCoin: \(formattedState).") + isValid = false + } else if !shouldHaveMobileCoin, + mobileCoin != nil { + owsFailDebug("Unexpected mobileCoin: \(formattedState).") + isValid = false + } + return isValid }