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

286 lines
9.8 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import LibSignalClient
public import GRDB
public struct ArchivedPayment: Codable, Hashable, FetchableRecord, PersistableRecord {
public static let databaseTableName: String = "ArchivedPayment"
public enum CodingKeys: String, CodingKey {
case id
case amount
case fee
case note
case mobileCoinIdentification
case status
case failureReason
case direction
case timestamp
case blockIndex
case blockTimestamp
case transaction
case receipt
case _senderOrRecipientAci = "senderOrRecipientAci"
case interactionUniqueId
}
public var id: Int64?
public var amount: String?
public var fee: String?
public var note: String?
public let mobileCoinIdentification: TransactionIdentifier?
public let status: Status
public let failureReason: FailureReason
public let direction: Direction
public let timestamp: UInt64?
public let blockIndex: UInt64?
public let blockTimestamp: UInt64?
public let transaction: Data?
public let receipt: Data?
private let _senderOrRecipientAci: UUID?
public let interactionUniqueId: String?
public var senderOrRecipientAci: Aci? {
_senderOrRecipientAci.map { Aci(fromUUID: $0) }
}
public init(
amount: String?,
fee: String?,
note: String?,
mobileCoinIdentification: TransactionIdentifier?,
status: Status,
failureReason: FailureReason,
direction: Direction,
timestamp: UInt64?,
blockIndex: UInt64?,
blockTimestamp: UInt64?,
transaction: Data?,
receipt: Data?,
senderOrRecipientAci: Aci?,
interactionUniqueId: String?
) {
self.amount = amount
self.fee = fee
self.note = note
self.mobileCoinIdentification = mobileCoinIdentification
self.status = status
self.failureReason = failureReason
self.direction = direction
self.timestamp = timestamp
self.blockIndex = blockIndex
self.blockTimestamp = blockTimestamp
self.transaction = transaction
self.receipt = receipt
self._senderOrRecipientAci = senderOrRecipientAci?.rawUUID
self.interactionUniqueId = interactionUniqueId
}
mutating public func didInsert(with rowID: Int64, for column: String?) {
id = rowID
}
public enum Direction: Int, Codable {
case unknown = 0
case incoming = 1
case outgoing = 2
}
public enum Status: Int, Codable {
case error = 0
case initial = 1
case submitted = 2
case successful = 3
public var isFailure: Bool {
return self == .error
}
}
public enum FailureReason: Int, Codable {
case none = 0
case genericFailure = 1
case networkFailure = 2
case insufficientFundsFailure = 3
}
public struct TransactionIdentifier: Codable, Hashable {
public let publicKey: [Data]?
public let keyImages: [Data]?
}
}
extension ArchivedPayment {
func toTransactionDetailsProto() -> BackupProto_PaymentNotification.TransactionDetails {
var transactionDetails = BackupProto_PaymentNotification.TransactionDetails()
if status.isFailure {
let reason: BackupProto_PaymentNotification.TransactionDetails.FailedTransaction.FailureReason = {
switch failureReason {
case .genericFailure: return .generic
case .networkFailure: return .network
case .insufficientFundsFailure: return .insufficientFunds
case .none:
owsFailDebug("Invalid status for failure type")
return .generic
}
}()
var failedTransaction = BackupProto_PaymentNotification.TransactionDetails.FailedTransaction()
failedTransaction.reason = reason
transactionDetails.payment = .failedTransaction(failedTransaction)
} else {
let success: BackupProto_PaymentNotification.TransactionDetails.Transaction.Status = {
switch status {
case .initial: return .initial
case .submitted: return .submitted
case .successful: return .successful
case .error:
owsFailDebug("Invalid status for success type")
return .successful
}
}()
var transaction = BackupProto_PaymentNotification.TransactionDetails.Transaction()
transaction.status = success
var identification = BackupProto_PaymentNotification.TransactionDetails.MobileCoinTxoIdentification()
switch direction {
case .incoming:
if let publicKey = mobileCoinIdentification?.publicKey {
identification.publicKey = publicKey
}
case .outgoing:
if let keyImages = mobileCoinIdentification?.keyImages {
identification.keyImages = keyImages
}
case .unknown:
owsFailDebug("Direction of payment not specified.")
}
transaction.mobileCoinIdentification = identification
if let timestamp { transaction.timestamp = timestamp }
if let blockIndex { transaction.blockIndex = blockIndex }
if let blockTimestamp { transaction.blockTimestamp = blockTimestamp }
if let _transaction = self.transaction { transaction.transaction = _transaction }
if let receipt { transaction.receipt = receipt }
transactionDetails.payment = .transaction(transaction)
}
return transactionDetails
}
}
extension TSPaymentModel {
func asArchivedPayment() -> ArchivedPayment {
var identifier: ArchivedPayment.TransactionIdentifier?
if let incomingTransactionPublicKeys = mobileCoin?.incomingTransactionPublicKeys {
identifier = ArchivedPayment.TransactionIdentifier(
publicKey: incomingTransactionPublicKeys,
keyImages: nil
)
} else if (mcOutputPublicKeys?.nilIfEmpty != nil || mcSpentKeyImages?.nilIfEmpty != nil) {
if (mcOutputPublicKeys?.nilIfEmpty == nil || mcSpentKeyImages?.nilIfEmpty == nil) {
owsFailDebug("one of public Keys or spent keys is nil")
identifier = nil
} else {
identifier = ArchivedPayment.TransactionIdentifier(
publicKey: nil,
keyImages: mcSpentKeyImages
)
}
}
let status: ArchivedPayment.Status = {
guard !isFailed else { return .error }
switch paymentState {
case .incomingComplete,
.incomingVerified,
.incomingUnverified,
.outgoingComplete,
.outgoingVerified,
.outgoingUnverified:
return .successful
case .outgoingSent,
.outgoingSending:
return .submitted
case .outgoingUnsubmitted:
return .initial
case .incomingFailed,
.outgoingFailed:
owsFailDebug("Invalid failure state in success path")
return .initial
@unknown default:
owsFailDebug("Encountered invalid payment state")
return .initial
}
}()
let failureReason: ArchivedPayment.FailureReason = {
guard isFailed else { return .none }
switch paymentFailure {
case .insufficientFunds:
return .insufficientFundsFailure
case .notificationSendFailed:
return .networkFailure
case .expired,
.invalid,
.validationFailed,
.unknown,
.none:
return .genericFailure
@unknown default:
owsFailDebug("Encountered invalid payment state")
return .genericFailure
}
}()
let direction: ArchivedPayment.Direction = {
switch paymentState {
case .incomingComplete,
.incomingVerified,
.incomingUnverified,
.incomingFailed,
.outgoingFailed:
return .incoming
case .outgoingComplete,
.outgoingVerified,
.outgoingUnverified,
.outgoingUnsubmitted,
.outgoingSent,
.outgoingSending:
return .outgoing
@unknown default:
owsFailDebug("Encountered invalid payment state")
return .unknown
}
}()
let formattedAmount = paymentAmount.map {PaymentsFormat.format(paymentAmount: $0, isShortForm: true)}
let formattedFee = mobileCoin?.feeAmount.map {PaymentsFormat.format(paymentAmount: $0, isShortForm: true)}
return ArchivedPayment(
amount: formattedAmount,
fee: formattedFee,
note: memoMessage,
mobileCoinIdentification: identifier,
status: status,
failureReason: failureReason,
direction: direction,
timestamp: createdTimestamp,
blockIndex: mcLedgerBlockIndex,
blockTimestamp: mcLedgerBlockTimestamp,
transaction: mcTransactionData,
receipt: mcReceiptData,
senderOrRecipientAci: senderOrRecipientAci?.wrappedAciValue,
interactionUniqueId: interactionUniqueId
)
}
}