TM-SGNL-iOS/SignalServiceKit/Payments/PaymentsHelperImpl.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

694 lines
32 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public import LibSignalClient
public class PaymentsHelperImpl: PaymentsHelperSwift, PaymentsHelper {
public init() {
self.observeNotifications()
}
private func observeNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(registrationStateDidChange),
name: .registrationStateDidChange,
object: nil)
}
@objc
private func registrationStateDidChange() {
// Caches should be re-warmed after a registration state change.
warmCaches()
}
public var isKillSwitchActive: Bool {
RemoteConfig.current.paymentsResetKillSwitch || !hasValidPhoneNumberForPayments
}
public var hasValidPhoneNumberForPayments: Bool {
guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
return false
}
guard let localNumber = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.phoneNumber else {
return false
}
let paymentsDisabledRegions = RemoteConfig.current.paymentsDisabledRegions
if paymentsDisabledRegions.isEmpty {
return Self.isValidPhoneNumberForPayments_fixedAllowlist(localNumber)
} else {
return Self.isValidPhoneNumberForPayments_remoteConfigBlocklist(localNumber, paymentsDisabledRegions: paymentsDisabledRegions)
}
}
private static func isValidPhoneNumberForPayments_fixedAllowlist(_ e164: String) -> Bool {
guard let phoneNumber = SSKEnvironment.shared.phoneNumberUtilRef.parseE164(e164) else {
owsFailDebug("Could not parse phone number: \(e164).")
return false
}
guard let callingCode = phoneNumber.getCallingCode() else {
owsFailDebug("Missing callingCode: \(e164).")
return false
}
let validCallingCodes: [Int] = [
// France
33,
// Switzerland
41,
// Parts of UK.
44,
// Germany
49
]
return validCallingCodes.contains(callingCode)
}
internal static func isValidPhoneNumberForPayments_remoteConfigBlocklist(
_ e164: String,
paymentsDisabledRegions: PhoneNumberRegions
) -> Bool {
owsAssertDebug(
!paymentsDisabledRegions.isEmpty,
"Missing paymentsDisabledRegions. Used the fixed allowlist instead."
)
return !paymentsDisabledRegions.contains(e164: e164)
}
public var canEnablePayments: Bool {
guard !isKillSwitchActive else {
return false
}
return hasValidPhoneNumberForPayments
}
// MARK: - PaymentsState
// NOTE: This k-v store is shared by PaymentsHelperImpl and PaymentsImpl.
fileprivate static let keyValueStore = KeyValueStore(collection: "Payments")
public var keyValueStore: KeyValueStore { Self.keyValueStore}
private static let arePaymentsEnabledKey = "isPaymentEnabled"
private static let paymentsEntropyKey = "paymentsEntropy"
private static let lastKnownLocalPaymentAddressProtoDataKey = "lastKnownLocalPaymentAddressProtoData"
private let paymentStateCache = AtomicOptional<PaymentsState>(nil, lock: .sharedGlobal)
public func warmCaches() {
owsAssertDebug(GRDBSchemaMigrator.areMigrationsComplete)
SSKEnvironment.shared.databaseStorageRef.read { transaction in
self.paymentStateCache.set(Self.loadPaymentsState(transaction: transaction))
}
}
public var paymentsState: PaymentsState {
paymentStateCache.get() ?? .disabled
}
public var arePaymentsEnabled: Bool {
paymentsState.isEnabled
}
public func arePaymentsEnabled(tx: SDSAnyReadTransaction) -> Bool {
Self.loadPaymentsState(transaction: tx).isEnabled
}
public var paymentsEntropy: Data? {
paymentsState.paymentsEntropy
}
public func enablePayments(transaction: SDSAnyWriteTransaction) {
// We must preserve any existing paymentsEntropy, and then prefer "old" entropy, then last resort generate new entropy.
let existingPaymentsEntropy = self.paymentsEntropy
let oldPaymentsEntropy = Self.loadPaymentsState(transaction: transaction).paymentsEntropy
let paymentsEntropy = existingPaymentsEntropy ?? oldPaymentsEntropy ?? Self.generateRandomPaymentsEntropy()
_ = enablePayments(withPaymentsEntropy: paymentsEntropy, transaction: transaction)
}
public func enablePayments(withPaymentsEntropy newPaymentsEntropy: Data, transaction: SDSAnyWriteTransaction) -> Bool {
let oldPaymentsEntropy = Self.loadPaymentsState(transaction: transaction).paymentsEntropy
guard oldPaymentsEntropy == nil || oldPaymentsEntropy == newPaymentsEntropy else {
owsFailDebug("paymentsEntropy is already set.")
return false
}
let paymentsState = PaymentsState.build(arePaymentsEnabled: true,
paymentsEntropy: newPaymentsEntropy)
owsAssertDebug(paymentsState.isEnabled)
setPaymentsState(paymentsState,
originatedLocally: true,
transaction: transaction)
owsAssertDebug(arePaymentsEnabled)
return true
}
public func disablePayments(transaction: SDSAnyWriteTransaction) {
switch paymentsState {
case .enabled(let paymentsEntropy):
setPaymentsState(.disabledWithPaymentsEntropy(paymentsEntropy: paymentsEntropy),
originatedLocally: true,
transaction: transaction)
case .disabled, .disabledWithPaymentsEntropy:
owsFailDebug("Payments already disabled.")
}
owsAssertDebug(!arePaymentsEnabled)
}
public func setPaymentsState(_ newPaymentsState: PaymentsState,
originatedLocally: Bool,
transaction: SDSAnyWriteTransaction) {
let oldPaymentsState = self.paymentsState
var newPaymentsState = newPaymentsState
// If payments was enabled remotely (e.g. on another device or previous install) we want
// to enable it even if the current device no longer supports enabling payments. This will
// behave as if the payments kill switch is turned on until the user is on a payments enabled
// install, but preserve their access to payments in the UI.
let canEnablePaymentsLocallyOrRemotely = self.canEnablePayments || !originatedLocally
if newPaymentsState.isEnabled && !canEnablePaymentsLocallyOrRemotely {
// If we cannot enable payments, ensure that any new entropy is always preserved.
if let paymentsEntropy = newPaymentsState.paymentsEntropy {
newPaymentsState = .disabledWithPaymentsEntropy(paymentsEntropy: paymentsEntropy)
} else {
newPaymentsState = .disabled
}
}
guard newPaymentsState != oldPaymentsState else {
return
}
if let oldPaymentsEntropy = oldPaymentsState.paymentsEntropy,
let newPaymentsEntropy = newPaymentsState.paymentsEntropy,
oldPaymentsEntropy != newPaymentsEntropy {
owsFailDebug("paymentsEntropy does not match.")
}
Self.keyValueStore.setBool(newPaymentsState.isEnabled,
key: Self.arePaymentsEnabledKey,
transaction: transaction.asV2Write)
if let paymentsEntropy = newPaymentsState.paymentsEntropy {
Self.keyValueStore.setData(paymentsEntropy,
key: Self.paymentsEntropyKey,
transaction: transaction.asV2Write)
}
self.paymentStateCache.set(newPaymentsState)
SSKEnvironment.shared.paymentsEventsRef.updateLastKnownLocalPaymentAddressProtoData(transaction: transaction)
let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction.asV2Read)?.aci
TSPaymentsActivationRequestModel
.allThreadsWithPaymentActivationRequests(transaction: transaction)
.forEach { thread in
// Only send out payments activated messages from the originating device.
if originatedLocally {
let message = OWSPaymentActivationRequestFinishedMessage(thread: thread, transaction: transaction)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: message
)
SSKEnvironment.shared.messageSenderJobQueueRef.add(
message: preparedMessage,
transaction: transaction
)
}
// But always insert an info message wherever we were requested.
if let localAci {
let infoMessage = TSInfoMessage(
thread: thread,
messageType: .paymentsActivated,
infoMessageUserInfo: [
.paymentActivatedAci: localAci.serviceIdString
]
)
infoMessage.anyInsert(transaction: transaction)
}
}
// Regardless of where it was originated, wipe the pending activation request state.
// Now that we have activated, they're useless.
_ = try? TSPaymentsActivationRequestModel.deleteAll(transaction.unwrapGrdbWrite.database)
transaction.addAsyncCompletionOffMain {
NotificationCenter.default.postNotificationNameAsync(PaymentsConstants.arePaymentsEnabledDidChange, object: nil)
SSKEnvironment.shared.paymentsEventsRef.paymentsStateDidChange()
if originatedLocally {
// We only need to re-upload the profile if the change originated
// locally.
Logger.info("Re-uploading local profile due to payments state change.")
SSKEnvironment.shared.profileManagerRef.reuploadLocalProfile(authedAccount: .implicit())
SSKEnvironment.shared.storageServiceManagerRef.recordPendingLocalAccountUpdates()
}
}
}
private static func loadPaymentsState(transaction: SDSAnyReadTransaction) -> PaymentsState {
guard DependenciesBridge.shared.tsAccountManager.registrationState(tx: transaction.asV2Read).isRegistered else {
return .disabled
}
let paymentsEntropy = keyValueStore.getData(paymentsEntropyKey, transaction: transaction.asV2Read)
let arePaymentsEnabled = keyValueStore.getBool(Self.arePaymentsEnabledKey,
defaultValue: false,
transaction: transaction.asV2Read)
return PaymentsState.build(arePaymentsEnabled: arePaymentsEnabled,
paymentsEntropy: paymentsEntropy)
}
private static func generateRandomPaymentsEntropy() -> Data {
Randomness.generateRandomBytes(PaymentsConstants.paymentsEntropyLength)
}
public func clearState(transaction: SDSAnyWriteTransaction) {
Self.keyValueStore.removeAll(transaction: transaction.asV2Write)
paymentStateCache.set(nil)
}
public func setLastKnownLocalPaymentAddressProtoData(_ data: Data?, transaction: SDSAnyWriteTransaction) {
Self.keyValueStore.setData(data, key: Self.lastKnownLocalPaymentAddressProtoDataKey, transaction: transaction.asV2Write)
}
public func lastKnownLocalPaymentAddressProtoData(transaction: SDSAnyWriteTransaction) -> Data? {
SSKEnvironment.shared.paymentsEventsRef.updateLastKnownLocalPaymentAddressProtoData(transaction: transaction)
return Self.keyValueStore.getData(Self.lastKnownLocalPaymentAddressProtoDataKey, transaction: transaction.asV2Read)
}
// MARK: -
private static let arePaymentsEnabledForUserStore = KeyValueStore(collection: "arePaymentsEnabledForUserStore")
public func setArePaymentsEnabled(for serviceId: ServiceId, hasPaymentsEnabled: Bool, transaction tx: SDSAnyWriteTransaction) {
Self.arePaymentsEnabledForUserStore.setBool(hasPaymentsEnabled, key: serviceId.serviceIdUppercaseString, transaction: tx.asV2Write)
}
public func arePaymentsEnabled(for address: SignalServiceAddress, transaction tx: SDSAnyReadTransaction) -> Bool {
guard let serviceId = address.serviceId else {
Logger.warn("User is missing serviceId.")
return false
}
return Self.arePaymentsEnabledForUserStore.getBool(
serviceId.serviceIdUppercaseString,
defaultValue: false,
transaction: tx.asV2Read
)
}
// MARK: - Version Compatibility
private let isPaymentsVersionOutdatedCache = AtomicValue<Bool>(false, lock: .sharedGlobal)
public var isPaymentsVersionOutdated: Bool {
isPaymentsVersionOutdatedCache.get()
}
public func setPaymentsVersionOutdated(_ value: Bool) {
let oldValue = isPaymentsVersionOutdatedCache.swap(value)
guard oldValue != value else { return }
NotificationCenter.default.postNotificationNameAsync(PaymentsConstants.isPaymentsVersionOutdatedDidChange, object: nil)
}
// MARK: - Incoming Messages
public func processIncomingPaymentNotification(
thread: TSThread,
paymentNotification: TSPaymentNotification,
senderAci: Aci,
transaction: SDSAnyWriteTransaction
) {
Logger.info("")
guard paymentNotification.isValid else {
owsFailDebug("Invalid paymentNotification.")
return
}
upsertPaymentModelForIncomingPaymentNotification(paymentNotification,
thread: thread,
senderAci: senderAci,
transaction: transaction)
}
public func processIncomingPaymentsActivationRequest(
thread: TSThread,
senderAci: Aci,
transaction: SDSAnyWriteTransaction
) {
Logger.info("")
// If we are activated already, immediately reply and finish.
// Only do this on the primary so we don't end up replying
// multiple times across every device that is requested.
if
Self.loadPaymentsState(transaction: transaction).isEnabled
{
if DependenciesBridge.shared.tsAccountManager.registrationState(tx: transaction.asV2Read).isPrimaryDevice ?? false {
let message = OWSPaymentActivationRequestFinishedMessage(thread: thread, transaction: transaction)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: message
)
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
}
return
}
// Create a model; we use this after we activate payments to
// know who requested we activate, so we can send them
// a message telling them we activated.
TSPaymentsActivationRequestModel.createIfNotExists(
threadUniqueId: thread.uniqueId,
senderAci: senderAci,
transaction: transaction
)
// Insert the info message to display in chat.
let infoMessage: TSInfoMessage = .paymentsActivationRequestMessage(
thread: thread,
senderAci: senderAci
)
infoMessage.anyInsert(transaction: transaction)
}
public func processIncomingPaymentsActivatedMessage(
thread: TSThread,
senderAci: Aci,
transaction: SDSAnyWriteTransaction
) {
Logger.info("")
let infoMessage: TSInfoMessage = .paymentsActivatedMessage(
thread: thread,
senderAci: senderAci
)
infoMessage.anyInsert(transaction: transaction)
setArePaymentsEnabled(
for: senderAci,
hasPaymentsEnabled: true,
transaction: transaction
)
}
public func processReceivedTranscriptPaymentNotification(
thread: TSThread,
paymentNotification: TSPaymentNotification,
messageTimestamp: UInt64,
transaction: SDSAnyWriteTransaction
) {
Logger.info("Ignoring payment notification from sync transcript.")
}
public func processIncomingPaymentSyncMessage(
_ paymentProto: SSKProtoSyncMessageOutgoingPayment,
messageTimestamp: UInt64,
transaction: SDSAnyWriteTransaction
) {
Logger.info("")
do {
guard let mobileCoinProto = paymentProto.mobileCoin else {
throw OWSAssertionError("Invalid payment sync message: Missing mobileCoinProto.")
}
var recipientAci: Aci?
if let recipientAciString = paymentProto.recipientServiceID {
guard let aci = Aci.parseFrom(aciString: recipientAciString) else {
throw OWSAssertionError("Invalid payment sync message: Missing recipientServiceID.")
}
recipientAci = aci
}
let paymentAmount = TSPaymentAmount(currency: .mobileCoin, picoMob: mobileCoinProto.amountPicoMob)
guard paymentAmount.isValidAmount(canBeEmpty: true) else {
throw OWSAssertionError("Invalid payment sync message: invalid paymentAmount.")
}
let feeAmount = TSPaymentAmount(currency: .mobileCoin, picoMob: mobileCoinProto.feePicoMob)
guard feeAmount.isValidAmount(canBeEmpty: false) else {
throw OWSAssertionError("Invalid payment sync message: invalid feeAmount.")
}
let recipientPublicAddressData = mobileCoinProto.recipientAddress
let memoMessage = paymentProto.note?.nilIfEmpty
let spentKeyImages = Array(Set(mobileCoinProto.spentKeyImages))
owsAssertDebug(spentKeyImages.count == mobileCoinProto.spentKeyImages.count)
guard !spentKeyImages.isEmpty else {
throw OWSAssertionError("Invalid payment sync message: Missing spentKeyImages.")
}
let outputPublicKeys = Array(Set(mobileCoinProto.outputPublicKeys))
owsAssertDebug(outputPublicKeys.count == mobileCoinProto.outputPublicKeys.count)
guard !outputPublicKeys.isEmpty else {
throw OWSAssertionError("Invalid payment sync message: Missing outputPublicKeys.")
}
guard let mcReceiptData = mobileCoinProto.receipt,
!mcReceiptData.isEmpty else {
throw OWSAssertionError("Invalid payment sync message: Missing or invalid receipt.")
}
_ = try SSKEnvironment.shared.mobileCoinHelperRef.info(forReceiptData: mcReceiptData)
let ledgerBlockIndex = mobileCoinProto.ledgerBlockIndex
guard ledgerBlockIndex > 0 else {
throw OWSAssertionError("Invalid payment sync message: Invalid ledgerBlockIndex.")
}
let ledgerBlockTimestamp = mobileCoinProto.ledgerBlockTimestamp
// We use .outgoingComplete. We can safely assume that the device which
// sent the payment has verified and notified.
let paymentState: TSPaymentState = .outgoingComplete
let paymentType: TSPaymentType
if recipientPublicAddressData == nil {
// Possible defragmentation.
guard recipientAci == nil else {
throw OWSAssertionError("Invalid payment sync message: unexpected recipientUuid.")
}
guard recipientPublicAddressData == nil else {
throw OWSAssertionError("Invalid payment sync message: unexpected recipientPublicAddressData.")
}
guard paymentAmount.isValidAmount(canBeEmpty: true),
paymentAmount.picoMob == 0 else {
throw OWSAssertionError("Invalid payment sync message: invalid paymentAmount.")
}
guard memoMessage == nil else {
throw OWSAssertionError("Invalid payment sync message: unexpected memoMessage.")
}
paymentType = .outgoingDefragmentationNotFromLocalDevice
} else {
// Possible outgoing payment.
guard recipientAci != nil else {
throw OWSAssertionError("Invalid payment sync message: missing recipientUuid.")
}
guard paymentAmount.isValidAmount(canBeEmpty: false) else {
throw OWSAssertionError("Invalid payment sync message: invalid paymentAmount.")
}
paymentType = .outgoingPaymentNotFromLocalDevice
}
let mobileCoin = MobileCoinPayment(recipientPublicAddressData: recipientPublicAddressData,
transactionData: nil,
receiptData: mcReceiptData,
incomingTransactionPublicKeys: nil,
spentKeyImages: spentKeyImages,
outputPublicKeys: outputPublicKeys,
ledgerBlockTimestamp: ledgerBlockTimestamp,
ledgerBlockIndex: ledgerBlockIndex,
feeAmount: feeAmount)
let paymentModel = TSPaymentModel(paymentType: paymentType,
paymentState: paymentState,
paymentAmount: paymentAmount,
createdDate: Date(millisecondsSince1970: messageTimestamp),
senderOrRecipientAci: recipientAci.map { AciObjC($0) },
memoMessage: memoMessage,
isUnread: false,
interactionUniqueId: nil,
mobileCoin: mobileCoin)
try tryToInsertPaymentModel(paymentModel, transaction: transaction)
// If we inserted without error, its new (no duplicates) so we should
// insert the outgoing message in chat.
if
paymentType == .outgoingPaymentNotFromLocalDevice,
let recipientAci,
recipientAci != DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction.asV2Read)?.aci
{
let thread = TSContactThread.getOrCreateThread(
withContactAddress: SignalServiceAddress(recipientAci),
transaction: transaction
)
let paymentNotification = TSPaymentNotification(
memoMessage: memoMessage,
mcReceiptData: mcReceiptData
)
let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
let dmConfig = dmConfigurationStore.fetchOrBuildDefault(for: .thread(thread), tx: transaction.asV2Read)
let message = OWSOutgoingPaymentMessage(
thread: thread,
messageBody: memoMessage,
paymentNotification: paymentNotification,
expiresInSeconds: dmConfig.durationSeconds,
expireTimerVersion: NSNumber(value: dmConfig.timerVersion),
transaction: transaction
)
message.anyInsert(transaction: transaction)
paymentModel.update(withInteractionUniqueId: message.uniqueId, transaction: transaction)
}
} catch {
owsFailDebug("Error: \(error)")
return
}
}
// This method enforces invariants around TSPaymentModel.
public func tryToInsertPaymentModel(
_ paymentModel: TSPaymentModel,
transaction: SDSAnyWriteTransaction
) throws {
Logger.info("Trying to insert: \(paymentModel.descriptionForLogs)")
guard paymentModel.isValid else {
throw OWSAssertionError("Invalid paymentModel.")
}
let isRedundant = try isProposedPaymentModelRedundant(paymentModel,
transaction: transaction)
guard !isRedundant else {
throw OWSAssertionError("Duplicate paymentModel.")
}
paymentModel.anyInsert(transaction: transaction)
}
// This method enforces invariants around TSPaymentModel.
private func isProposedPaymentModelRedundant(
_ paymentModel: TSPaymentModel,
transaction: SDSAnyWriteTransaction
) throws -> Bool {
guard paymentModel.isValid else {
throw OWSAssertionError("Invalid paymentModel.")
}
// Only one model in the database should have a given transaction.
if paymentModel.canHaveMCTransaction {
if let transactionData = paymentModel.mobileCoin?.transactionData {
let existingPaymentModels = PaymentFinder.paymentModels(forMcTransactionData: transactionData,
transaction: transaction)
if existingPaymentModels.count > 1 {
owsFailDebug("More than one conflict.")
}
if !existingPaymentModels.isEmpty {
owsFailDebug("Transaction conflict.")
return true
}
} else if paymentModel.shouldHaveMCTransaction {
throw OWSAssertionError("Missing transactionData.")
}
}
// Only one model in the database should have a given receipt.
if paymentModel.shouldHaveMCReceipt {
if let receiptData = paymentModel.mobileCoin?.receiptData {
let existingPaymentModels = PaymentFinder.paymentModels(forMcReceiptData: receiptData,
transaction: transaction)
if existingPaymentModels.count > 1 {
owsFailDebug("More than one conflict.")
}
if !existingPaymentModels.isEmpty {
owsFailDebug("Receipt conflict.")
return true
}
} else {
throw OWSAssertionError("Missing receiptData.")
}
}
// Only one _identified_ payment model in the database should correspond to any given
// spentKeyImage or outputPublicKey.
//
// We don't need to worry about conflicts with unidentified payment models;
// PaymentsReconciliation will avoid / clean those up.
let mcLedgerBlockIndex = paymentModel.mobileCoin?.ledgerBlockIndex ?? 0
let spentKeyImages = Set(paymentModel.mobileCoin?.spentKeyImages ?? [])
let outputPublicKeys = Set(paymentModel.mobileCoin?.outputPublicKeys ?? [])
if !paymentModel.isUnidentified,
mcLedgerBlockIndex > 0 {
let otherPaymentModels = PaymentFinder.paymentModels(forMcLedgerBlockIndex: mcLedgerBlockIndex,
transaction: transaction)
for otherPaymentModel in otherPaymentModels {
guard !otherPaymentModel.isUnidentified else {
continue
}
guard paymentModel.uniqueId != otherPaymentModel.uniqueId else {
owsFailDebug("Duplicate paymentModel.")
return true
}
let otherSpentKeyImages = otherPaymentModel.mobileCoin?.spentKeyImages ?? []
let otherOutputPublicKeys = otherPaymentModel.mobileCoin?.outputPublicKeys ?? []
if !spentKeyImages.isDisjoint(with: otherSpentKeyImages) {
owsFailDebug("spentKeyImage conflict.")
return true
}
if !outputPublicKeys.isDisjoint(with: otherOutputPublicKeys) {
owsFailDebug("outputPublicKey conflict.")
return true
}
}
}
return false
}
// MARK: - Upsert Payment Records
private func upsertPaymentModelForIncomingPaymentNotification(_ paymentNotification: TSPaymentNotification,
thread: TSThread,
senderAci: Aci,
transaction: SDSAnyWriteTransaction) {
do {
let mcReceiptData = paymentNotification.mcReceiptData
let receiptInfo = try SSKEnvironment.shared.mobileCoinHelperRef.info(forReceiptData: mcReceiptData)
let mobileCoin = MobileCoinPayment(recipientPublicAddressData: nil,
transactionData: nil,
receiptData: paymentNotification.mcReceiptData,
incomingTransactionPublicKeys: [ receiptInfo.txOutPublicKey ],
spentKeyImages: nil,
outputPublicKeys: nil,
ledgerBlockTimestamp: 0,
ledgerBlockIndex: 0,
feeAmount: nil)
let paymentModel = TSPaymentModel(paymentType: .incomingPayment,
paymentState: .incomingUnverified,
paymentAmount: nil,
createdDate: Date(),
senderOrRecipientAci: AciObjC(senderAci),
memoMessage: paymentNotification.memoMessage?.nilIfEmpty,
isUnread: true,
interactionUniqueId: nil,
mobileCoin: mobileCoin)
guard paymentModel.isValid else {
throw OWSAssertionError("Invalid paymentModel.")
}
try tryToInsertPaymentModel(paymentModel, transaction: transaction)
// TODO: Remove any corresponding payment request.
} catch {
owsFailDebug("Error: \(error)")
}
}
}
// MARK: -
public struct PaymentsPassphrase: Equatable {
public let words: [String]
public init(words: [String]) throws {
guard words.count == PaymentsConstants.passphraseWordCount else {
owsFailDebug("words.count \(words.count) != \(PaymentsConstants.passphraseWordCount)")
throw PaymentsError.invalidPassphrase
}
self.words = words
}
public var wordCount: Int { words.count }
public var asPassphrase: String { words.joined(separator: " ") }
public var debugDescription: String { asPassphrase }
}