diff --git a/Podfile b/Podfile index 565fd932f4..d048db0ef7 100644 --- a/Podfile +++ b/Podfile @@ -76,8 +76,8 @@ pod 'BonMot', inhibit_warnings: true # For catalyst we need to be on master until 3.6.7 or later is released pod 'ZXingObjC', git: 'https://github.com/zxingify/zxingify-objc.git', inhibit_warnings: true, binary: true -pod 'MobileCoin', git: 'https://github.com/mobilecoinofficial/MobileCoin-Swift.git' -# pod 'MobileCoin', path: '../MobileCoin-Swift' +# pod 'MobileCoin', git: 'https://github.com/mobilecoinofficial/MobileCoin-Swift.git' +pod 'MobileCoin', path: '../MobileCoin-Swift' target 'Signal' do project 'Signal.xcodeproj', 'Debug' => :debug, 'Release' => :release diff --git a/Podfile.lock b/Podfile.lock index 20d2879d3a..528fc29a2a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -234,7 +234,7 @@ DEPENDENCIES: - libPhoneNumber-iOS (from `https://github.com/signalapp/libPhoneNumber-iOS`, branch `signal-master`) - lottie-ios - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`) - - MobileCoin (from `https://github.com/mobilecoinofficial/MobileCoin-Swift.git`) + - MobileCoin (from `../MobileCoin-Swift`) - PromiseKit - PureLayout - Reachability @@ -313,7 +313,7 @@ EXTERNAL SOURCES: :branch: signal-master :git: https://github.com/signalapp/Mantle MobileCoin: - :git: https://github.com/mobilecoinofficial/MobileCoin-Swift.git + :path: "../MobileCoin-Swift" SignalArgon2: :git: https://github.com/signalapp/Argon2.git :submodules: true @@ -354,9 +354,16 @@ CHECKOUT OPTIONS: Mantle: :commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4 :git: https://github.com/signalapp/Mantle +<<<<<<< HEAD MobileCoin: :commit: deedb518af67817bfbf3c67e2cf3ab87c8463e85 :git: https://github.com/mobilecoinofficial/MobileCoin-Swift.git +||||||| parent of cfce6c84da... Update MC SDK. + MobileCoin: + :commit: 57b9da09203916b8bc67fc40a2b0dd06fb705df8 + :git: https://github.com/mobilecoinofficial/MobileCoin-Swift.git +======= +>>>>>>> cfce6c84da... Update MC SDK. SignalArgon2: :commit: 6a37e32167500ff7830e337b726a1653061d86b2 :git: https://github.com/signalapp/Argon2.git @@ -435,6 +442,12 @@ SPEC CHECKSUMS: ZKGroup: 15a08eed5870a44b9dd14af0824ac95f6eed7380 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb +<<<<<<< HEAD PODFILE CHECKSUM: b9765e2bc1ac90c71ecfb8a54c0afd8a1b7281ca +||||||| parent of cfce6c84da... Update MC SDK. +PODFILE CHECKSUM: 021a74040108d7fd42587934c29307c660065276 +======= +PODFILE CHECKSUM: 932b6d2360c8f11321e233bd8238bcd05a3160f0 +>>>>>>> cfce6c84da... Update MC SDK. COCOAPODS: 1.9.2 diff --git a/SignalMessaging/Payments/MobileCoinAPI.swift b/SignalMessaging/Payments/MobileCoinAPI.swift index 9ffe162323..a3b482bd3e 100644 --- a/SignalMessaging/Payments/MobileCoinAPI.swift +++ b/SignalMessaging/Payments/MobileCoinAPI.swift @@ -162,8 +162,9 @@ class MobileCoinAPI { let fogView: Attestation let fogKeyImage: Attestation let fogMerkleProof: Attestation - let fogIngest: Attestation + let fogReport: Attestation + // PAYMENTS TODO: What are the correct values? static let CONSENSUS_PRODUCT_ID: UInt16 = 1 static let CONSENSUS_SECURITY_VERSION: UInt16 = 1 static let FOG_VIEW_PRODUCT_ID: UInt16 = 3 @@ -172,9 +173,32 @@ class MobileCoinAPI { static let FOG_LEDGER_SECURITY_VERSION: UInt16 = 1 static let FOG_INGEST_PRODUCT_ID: UInt16 = 4 static let FOG_INGEST_SECURITY_VERSION: UInt16 = 1 + // PAYMENTS TODO: What are the correct values? + static let FOG_REPORT_PRODUCT_ID: UInt16 = 4 + static let FOG_REPORT_SECURITY_VERSION: UInt16 = 1 static var allowedHardeningAdvisories: [String] { ["INTEL-SA-00334"] } + // PAYMENTS TODO: What are the correct values? + private static func buildAttestation(mrSigner: Data, + productId: UInt16, + minimumSecurityVersion: UInt16, + allowedConfigAdvisories: [String] = [], + allowedHardeningAdvisories: [String] = []) throws -> MobileCoin.Attestation { + let result = MobileCoin.Attestation.make(mrSigner: mrSigner, + productId: productId, + minimumSecurityVersion: minimumSecurityVersion, + allowedConfigAdvisories: allowedConfigAdvisories, + allowedHardeningAdvisories: allowedHardeningAdvisories) + switch result { + case .success(let attestation): + return attestation + case .failure(let error): + owsFailDebug("Error: \(error)") + throw error + } + } + static var signalStaging: OWSAttestationConfig { // PAYMENTS TODO: let phonyMrSigner = Data([ @@ -182,32 +206,34 @@ class MobileCoinAPI { 121, 244, 252, 42, 166, 188, 185, 147, 1, 44, 60, 56, 108 ]) + // PAYMENTS TODO: What are the correct values? do { return OWSAttestationConfig( - consensus: try Attestation( + consensus: try buildAttestation( mrSigner: phonyMrSigner, productId: CONSENSUS_PRODUCT_ID, minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories), - fogView: try Attestation( + fogView: try buildAttestation( mrSigner: phonyMrSigner, productId: FOG_VIEW_PRODUCT_ID, - minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, + minimumSecurityVersion: FOG_VIEW_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories), - fogKeyImage: try Attestation( + fogKeyImage: try buildAttestation( mrSigner: phonyMrSigner, productId: FOG_LEDGER_PRODUCT_ID, - minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, + minimumSecurityVersion: FOG_LEDGER_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories), - fogMerkleProof: try Attestation( + fogMerkleProof: try buildAttestation( mrSigner: phonyMrSigner, productId: FOG_LEDGER_PRODUCT_ID, - minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, + minimumSecurityVersion: FOG_INGEST_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories), - fogIngest: try Attestation( + // TODO: + fogReport: try buildAttestation( mrSigner: phonyMrSigner, - productId: FOG_INGEST_PRODUCT_ID, - minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, + productId: FOG_REPORT_PRODUCT_ID, + minimumSecurityVersion: FOG_REPORT_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories)) } catch { owsFail("Invalid attestationConfig: \(error)") @@ -221,32 +247,34 @@ class MobileCoinAPI { 121, 244, 252, 42, 166, 188, 185, 147, 1, 44, 60, 56, 108 ]) + // PAYMENTS TODO: What are the correct values? do { return OWSAttestationConfig( - consensus: try Attestation( + consensus: try buildAttestation( mrSigner: phonyMrSigner, productId: CONSENSUS_PRODUCT_ID, minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories), - fogView: try Attestation( + fogView: try buildAttestation( mrSigner: phonyMrSigner, productId: FOG_VIEW_PRODUCT_ID, - minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, + minimumSecurityVersion: FOG_VIEW_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories), - fogKeyImage: try Attestation( + fogKeyImage: try buildAttestation( mrSigner: phonyMrSigner, productId: FOG_LEDGER_PRODUCT_ID, - minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, + minimumSecurityVersion: FOG_LEDGER_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories), - fogMerkleProof: try Attestation( + fogMerkleProof: try buildAttestation( mrSigner: phonyMrSigner, productId: FOG_LEDGER_PRODUCT_ID, - minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, + minimumSecurityVersion: FOG_INGEST_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories), - fogIngest: try Attestation( + // TODO: + fogReport: try buildAttestation( mrSigner: phonyMrSigner, - productId: FOG_INGEST_PRODUCT_ID, - minimumSecurityVersion: CONSENSUS_SECURITY_VERSION, + productId: FOG_REPORT_PRODUCT_ID, + minimumSecurityVersion: FOG_REPORT_SECURITY_VERSION, allowedHardeningAdvisories: allowedHardeningAdvisories)) } catch { owsFail("Invalid attestationConfig: \(error)") @@ -289,37 +317,47 @@ class MobileCoinAPI { fileprivate func buildClient(signalAuthorization: OWSAuthorization) throws -> MobileCoinClient { let networkConfig = MobileCoinNetworkConfig.networkConfig(environment: environment) - let client: MobileCoinClient let authorization: OWSAuthorization + let configResult: Swift.Result switch environment { case .signalProduction, .signalStaging: authorization = signalAuthorization let attestationConfig = OWSAttestationConfig.attestationConfig(environment: environment) - let config = try MobileCoinClient.Config(consensusUrl: networkConfig.consensusUrl, - consensusAttestation: attestationConfig.consensus, - fogViewUrl: networkConfig.fogViewUrl, - fogViewAttestation: attestationConfig.fogView, - fogLedgerUrl: networkConfig.fogLedgerUrl, - fogKeyImageAttestation: attestationConfig.fogKeyImage, - fogMerkleProofAttestation: attestationConfig.fogMerkleProof, - fogIngestAttestation: attestationConfig.fogIngest) - client = try MobileCoinClient(accountKey: accountKey, config: config) + configResult = MobileCoinClient.Config.make(consensusUrl: networkConfig.consensusUrl, + consensusAttestation: attestationConfig.consensus, + fogViewUrl: networkConfig.fogViewUrl, + fogViewAttestation: attestationConfig.fogView, + fogLedgerUrl: networkConfig.fogLedgerUrl, + fogKeyImageAttestation: attestationConfig.fogKeyImage, + fogMerkleProofAttestation: attestationConfig.fogMerkleProof, + fogReportAttestation: attestationConfig.fogReport) case .mobileCoinAlphaNet: authorization = OWSAuthorization.mobileCoinAlpha - let config = try MobileCoinClient.Config(consensusUrl: networkConfig.consensusUrl, - fogViewUrl: networkConfig.fogViewUrl, - fogLedgerUrl: networkConfig.fogLedgerUrl) - client = try MobileCoinClient(accountKey: accountKey, config: config) + configResult = MobileCoinClient.Config.make(consensusUrl: networkConfig.consensusUrl, + fogViewUrl: networkConfig.fogViewUrl, + fogLedgerUrl: networkConfig.fogLedgerUrl) case .mobileCoinMobileDev: authorization = OWSAuthorization.mobileCoinMobileDev - let config = try MobileCoinClient.Config(consensusUrl: networkConfig.consensusUrl, - fogViewUrl: networkConfig.fogViewUrl, - fogLedgerUrl: networkConfig.fogLedgerUrl) - client = try MobileCoinClient(accountKey: accountKey, config: config) + configResult = MobileCoinClient.Config.make(consensusUrl: networkConfig.consensusUrl, + fogViewUrl: networkConfig.fogViewUrl, + fogLedgerUrl: networkConfig.fogLedgerUrl) + } + switch configResult { + case .success(let config): + let clientResult = MobileCoinClient.make(accountKey: accountKey, config: config) + switch clientResult { + case .success(let client): + client.setBasicAuthorization(username: authorization.username, + password: authorization.password) + return client + case .failure(let error): + owsFailDebug("Error: \(error)") + throw error + } + case .failure(let error): + owsFailDebug("Error: \(error)") + throw error } - client.setBasicAuthorization(username: authorization.username, - password: authorization.password) - return client } } @@ -367,8 +405,8 @@ class MobileCoinAPI { let fogReportId = "" let result = MobileCoin.AccountKey.make(rootEntropy: rootEntropy, fogReportUrl: networkConfig.fogReportUrl, - fogAuthoritySpki: fogAuthoritySpki, - fogReportId: fogReportId) + fogReportId: fogReportId, + fogAuthoritySpki: fogAuthoritySpki) switch result { case .success(let accountKey): return accountKey @@ -420,65 +458,33 @@ class MobileCoinAPI { } } - func getMinimumFee(forPaymentAmount paymentAmount: TSPaymentAmount) -> Promise { + func getEstimatedFee(forPaymentAmount paymentAmount: TSPaymentAmount) throws -> TSPaymentAmount { Logger.verbose("") - let client = self.client + guard paymentAmount.isValidAmount(canBeEmpty: false) else { + throw OWSAssertionError("Invalid amount.") + } - return firstly(on: .global()) { () throws -> Promise in - guard paymentAmount.currency == .mobileCoin else { - throw OWSAssertionError("Invalid currency.") - } - guard paymentAmount.picoMob > 0 else { + // We don't need to support amountPicoMobHigh. + // + // TODO: Are we always going to use _minimum_ fee? + let result = client.estimateTotalFee(toSendAmount: paymentAmount.picoMob, + feeLevel: .minimum) + switch result { + case .success(let feePicoMob): + let fee = TSPaymentAmount(currency: .mobileCoin, picoMob: feePicoMob) + guard fee.isValidAmount(canBeEmpty: false) else { throw OWSAssertionError("Invalid amount.") } - - let (promise, resolver) = Promise.pending() - if DebugFlags.paymentsNoRequestsComplete.get() { - // Never resolve. - return promise - } - // We don't need to support amountPicoMobHigh. - // - // TODO: Are we always going to use _minimum_ fee? - client.minimumFee(amount: paymentAmount.picoMob) { (result: Swift.Result) in - switch result { - case .success(let feePicoMob): - let fee = TSPaymentAmount(currency: .mobileCoin, picoMob: feePicoMob) - guard paymentAmount.currency == .mobileCoin, - fee.currency == .mobileCoin else { - resolver.reject(OWSAssertionError("Invalid currency.")) - return - } - guard paymentAmount.picoMob > 0, - fee.picoMob > 0 else { - resolver.reject(OWSAssertionError("Invalid amount.")) - return - } - Logger.verbose("Success paymentAmount: \(paymentAmount), fee: \(fee), ") - resolver.fulfill(fee) - case .failure(let error): - let error = Self.convertMCError(error: error) - resolver.reject(error) - } - } - return promise - }.map(on: .global()) { (value: TSPaymentAmount) -> TSPaymentAmount in - Logger.verbose("Success: \(value)") - return value - }.recover(on: .global()) { (error: Error) -> Promise in + Logger.verbose("Success paymentAmount: \(paymentAmount), fee: \(fee), ") + return fee + case .failure(let error): + let error = Self.convertMCError(error: error) owsFailDebugUnlessMCNetworkFailure(error) throw error - }.timeout(seconds: Self.timeoutDuration, description: "prepareTransaction") { () -> Error in - PaymentsError.timeout } } - func getEstimatedFee(forPaymentAmount paymentAmount: TSPaymentAmount) -> Promise { - // TODO: Use proper SDK method when ready. - getMinimumFee(forPaymentAmount: paymentAmount) - } - struct PreparedTransaction { let transaction: MobileCoin.Transaction let receipt: MobileCoin.Receipt @@ -494,9 +500,9 @@ class MobileCoinAPI { return firstly(on: .global()) { () throws -> Promise in // prepareTransaction() will fail if local balance is not yet known. self.getLocalBalance() - }.then(on: .global()) { (balance: TSPaymentAmount) -> Promise in + }.map(on: .global()) { (balance: TSPaymentAmount) -> TSPaymentAmount in Logger.verbose("balance: \(balance.picoMob)") - return self.getMinimumFee(forPaymentAmount: paymentAmount) + return try self.getEstimatedFee(forPaymentAmount: paymentAmount) }.then(on: .global()) { (feeAmount: TSPaymentAmount) -> Promise in guard paymentAmount.isValidAmount(canBeEmpty: false) else { throw OWSAssertionError("Invalid amount.") @@ -551,7 +557,7 @@ class MobileCoinAPI { return promise } let client = self.client - client.submitTransaction(transaction) { (result: Swift.Result) in + client.submitTransaction(transaction) { (result: Swift.Result) in switch result { case .success: resolver.fulfill(()) @@ -581,7 +587,12 @@ class MobileCoinAPI { } let client = self.client - return firstly(on: .global()) { () throws -> Promise in + return firstly(on: .global()) { () throws -> Promise in + // .status(of: transaction) requires an updated balance. + // + // TODO: We could improve perf when verifying multiple transactions by getting balance just once. + self.getLocalBalance() + }.then(on: .global()) { (_: TSPaymentAmount) -> Promise in let (promise, resolver) = Promise.pending() if DebugFlags.paymentsNoRequestsComplete.get() { // Never resolve. @@ -638,38 +649,34 @@ class MobileCoinAPI { let client = self.client let localAccount = self.localAccount - return firstly(on: .global()) { () throws -> Promise in + return firstly(on: .global()) { () throws -> Promise in + // .status(of: receipt) requires an updated balance. + // + // TODO: We could improve perf when verifying multiple receipts by getting balance just once. + self.getLocalBalance() + }.map(on: .global()) { (_: TSPaymentAmount) -> MCIncomingReceiptStatus in let paymentAmount: TSPaymentAmount do { paymentAmount = try Self.paymentAmount(forReceipt: receipt, localAccount: localAccount) } catch { owsFailDebug("Error: \(error)") - return Promise.value(MCIncomingReceiptStatus(receiptStatus: .failed, - paymentAmount: .zeroMob, - txOutPublicKey: Data())) + return MCIncomingReceiptStatus(receiptStatus: .failed, + paymentAmount: .zeroMob, + txOutPublicKey: Data()) } let txOutPublicKey: Data = receipt.txOutPublicKey - let (promise, resolver) = Promise.pending() - if DebugFlags.paymentsNoRequestsComplete.get() { - // Never resolve. - return promise + let result = client.status(of: receipt) + switch result { + case .success(let receiptStatus): + return MCIncomingReceiptStatus(receiptStatus: receiptStatus, + paymentAmount: paymentAmount, + txOutPublicKey: txOutPublicKey) + case .failure(let error): + let error = Self.convertMCError(error: error) + throw error } - client.status(of: receipt) { (result: Swift.Result) in - switch result { - case .success(let receiptStatus): - resolver.fulfill(MCIncomingReceiptStatus(receiptStatus: receiptStatus, - paymentAmount: paymentAmount, - txOutPublicKey: txOutPublicKey)) - break - case .failure(let error): - let error = Self.convertMCError(error: error) - resolver.reject(error) - break - } - } - return promise }.map(on: .global()) { (value: MCIncomingReceiptStatus) -> MCIncomingReceiptStatus in Logger.verbose("Success: \(value)") return value @@ -788,18 +795,31 @@ extension MobileCoinAPI { case .insufficientBalance: Logger.warn("Error: \(error)") return PaymentsError.insufficientFunds + case .defragmentationRequired: + Logger.warn("Error: \(error)") + return PaymentsError.defragmentationRequired case .connectionError(let connectionError): // Recurse. return convertMCError(error: connectionError) } - case let error as MobileCoin.ReceiptStatusCheckError: + case let error as MobileCoin.TransactionSubmissionError: switch error { - case .invalidReceipt(let invalidInputError): - // Recurse. - return convertMCError(error: invalidInputError) case .connectionError(let connectionError): // Recurse. return convertMCError(error: connectionError) + case .invalidTransaction: + Logger.warn("Error: \(error)") + return PaymentsError.invalidTransaction + case .feeError: + Logger.warn("Error: \(error)") + return PaymentsError.invalidFee + case .tombstoneBlockTooFar: + Logger.warn("Error: \(error)") + // Map to .invalidTransaction + return PaymentsError.invalidTransaction + case .inputsAlreadySpent: + Logger.warn("Error: \(error)") + return PaymentsError.inputsAlreadySpent } default: owsFailDebug("Unexpected error: \(error)") @@ -834,7 +854,10 @@ public extension PaymentsError { .serializationError, .verificationStatusUnknown, .ledgerBlockTimestampUnknown, - .missingModel: + .missingModel, + .defragmentationRequired, + .invalidTransaction, + .inputsAlreadySpent: return false case .connectionFailure, .timeout: diff --git a/SignalMessaging/Payments/PaymentsImpl.swift b/SignalMessaging/Payments/PaymentsImpl.swift index a8942a131f..0865da2ddb 100644 --- a/SignalMessaging/Payments/PaymentsImpl.swift +++ b/SignalMessaging/Payments/PaymentsImpl.swift @@ -820,8 +820,8 @@ public extension PaymentsImpl { return firstly(on: .global()) { () -> Promise in self.getMobileCoinAPI() - }.then(on: .global()) { (mobileCoinAPI: MobileCoinAPI) -> Promise in - mobileCoinAPI.getEstimatedFee(forPaymentAmount: paymentAmount) + }.map(on: .global()) { (mobileCoinAPI: MobileCoinAPI) -> TSPaymentAmount in + try mobileCoinAPI.getEstimatedFee(forPaymentAmount: paymentAmount) } } diff --git a/SignalMessaging/Payments/PaymentsProcessor.swift b/SignalMessaging/Payments/PaymentsProcessor.swift index b7b73147fa..7b770a4606 100644 --- a/SignalMessaging/Payments/PaymentsProcessor.swift +++ b/SignalMessaging/Payments/PaymentsProcessor.swift @@ -542,7 +542,9 @@ private class PaymentProcessingOperation: OWSOperation { .serializationError, .missingModel, .connectionFailure, - .timeout: + .timeout, + .invalidTransaction, + .inputsAlreadySpent: owsFailDebugUnlessMCNetworkFailure(error) case .authorizationFailure: owsFailDebugUnlessMCNetworkFailure(error) @@ -553,6 +555,9 @@ private class PaymentProcessingOperation: OWSOperation { .ledgerBlockTimestampUnknown: // These errors are expected. Logger.info("Error: \(error)") + case .defragmentationRequired: + // These errors are expected but should be very rare. + owsFailDebugUnlessMCNetworkFailure(error) } default: owsFailDebugUnlessMCNetworkFailure(error) @@ -590,7 +595,9 @@ private class PaymentProcessingOperation: OWSOperation { .attestationVerificationFailed, .outdatedClient, .serializationError, - .missingModel: + .missingModel, + .invalidTransaction, + .inputsAlreadySpent: // Do not retry these errors. delegate?.endProcessing(paymentId: self.paymentId) case .serverRateLimited: @@ -633,6 +640,13 @@ private class PaymentProcessingOperation: OWSOperation { delegate?.scheduleRetryProcessing(paymentModel: paymentModel, retryDelayInteral: retryDelayInteral, nextRetryDelayInteral: nextRetryDelayInteral) + case .defragmentationRequired: + // Vanilla exponential backoff. + let retryDelayInteral = self.retryDelayInteral + let nextRetryDelayInteral = self.retryDelayInteral * 2 + delegate?.scheduleRetryProcessing(paymentModel: paymentModel, + retryDelayInteral: retryDelayInteral, + nextRetryDelayInteral: nextRetryDelayInteral) } default: // Do not retry assertion errors. @@ -757,19 +771,20 @@ private class PaymentProcessingOperation: OWSOperation { Self.payments.getMobileCoinAPI() }.then(on: .global()) { (mobileCoinAPI: MobileCoinAPI) -> Promise in return mobileCoinAPI.submitTransaction(transaction: transaction) - }.map(on: .global()) { _ in - try Self.databaseStorage.write { transaction in - try paymentModel.updatePaymentModelState(fromState: .outgoingUnsubmitted, - toState: .outgoingUnverified, - transaction: transaction) - } + }.then(on: .global()) { _ in + Self.updatePaymentStatePromise(paymentModel: paymentModel, + fromState: .outgoingUnsubmitted, + toState: .outgoingUnverified) }.recover(on: .global()) { (error: Error) -> Promise in - // Payments TODO: We need to handle certain errors here. - // e.g. if we double-submit a transaction, it should become unverified, - // not stuck in unsubmitted. - // - // Other errors might make this failed? - throw error + if case PaymentsError.inputsAlreadySpent = error { + // e.g. if we double-submit a transaction, it should become unverified, + // not stuck in unsubmitted. + return Self.updatePaymentStatePromise(paymentModel: paymentModel, + fromState: .outgoingUnsubmitted, + toState: .outgoingUnverified) + } else { + throw error + } } } @@ -797,7 +812,7 @@ private class PaymentProcessingOperation: OWSOperation { try Self.databaseStorage.write { transaction in switch transactionStatus.transactionStatus { - case .unknown, .pending: + case .unknown: throw PaymentsError.verificationStatusUnknown case .accepted(let block): if !paymentModel.hasMCLedgerBlockIndex { @@ -1091,10 +1106,6 @@ private class PaymentProcessingOperation: OWSOperation { // This should never happen, since we've already verified the transaction. owsFailDebug("Unexpected receiptStatus for verified transaction: .unknown") throw PaymentsError.verificationStatusUnknown - case .pending: - // This should never happen, since we've already verified the transaction. - owsFailDebug("Unexpected receiptStatus for verified transaction: .pending") - throw PaymentsError.verificationStatusUnknown case .accepted(let block): guard let ledgerBlockDate = block.timestamp else { Logger.warn("Could not fill in ledger timestamp for transaction.") diff --git a/SignalServiceKit/src/Payments/Payments.swift b/SignalServiceKit/src/Payments/Payments.swift index 9689c41742..560ab02103 100644 --- a/SignalServiceKit/src/Payments/Payments.swift +++ b/SignalServiceKit/src/Payments/Payments.swift @@ -30,6 +30,9 @@ public enum PaymentsError: Error { case verificationStatusUnknown case ledgerBlockTimestampUnknown case missingModel + case defragmentationRequired + case invalidTransaction + case inputsAlreadySpent } // MARK: -