commit ff9e44da222a6243a1cdeaa83fa16f473d1eac60
parent 01954d08f06a48cd9db0bc4ab2fcd49865593d3b
Author: Marc Stibane <marc@taler.net>
Date: Thu, 13 Aug 2026 15:49:05 +0200
AI: try to guard against wallet-core JSON errors
Diffstat:
2 files changed, 47 insertions(+), 0 deletions(-)
diff --git a/TalerWallet1/Model/Model+Transactions.swift b/TalerWallet1/Model/Model+Transactions.swift
@@ -8,6 +8,7 @@
import Foundation
import taler_swift
import SymLog
+import os.log
// MARK: -
extension WalletModel {
@@ -60,6 +61,35 @@ fileprivate struct GetTransactionsV2: WalletBackendFormattedRequest {
struct Response: Decodable { // list of transactions
var transactions: [TalerTransaction]
+
+ enum CodingKeys: String, CodingKey { case transactions }
+
+ /// Decode the array element by element so that ONE transaction wallet-core sends
+ /// in a shape this app does not understand costs us that transaction, not the
+ /// user's entire history. Decoding `[TalerTransaction]` directly throws on the
+ /// first bad element and every caller's `try?` then renders an empty list.
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ let wrapped = try container.decode([SkippableTransaction].self, forKey: .transactions)
+ self.transactions = wrapped.compactMap { $0.transaction }
+ }
+ }
+}
+
+/// Wrapper whose own decoding never fails, so a failed element consumes its array slot
+/// instead of aborting the array. See GetTransactionsV2.Response.
+fileprivate struct SkippableTransaction: Decodable {
+ let transaction: TalerTransaction?
+
+ init(from decoder: Decoder) throws {
+ do {
+ transaction = try TalerTransaction(from: decoder)
+ } catch {
+ transaction = nil
+ let details = String(describing: error)
+ Logger(subsystem: "net.taler.gnu", category: "Transactions")
+ .error("dropped an undecodable transaction: \(details, privacy: .public)")
+ }
}
}
/// A request to abort a wallet transaction by ID.
diff --git a/TalerWallet1/Model/Transaction.swift b/TalerWallet1/Model/Transaction.swift
@@ -20,6 +20,7 @@ enum TransactionDecodingError: Error {
}
enum TransactionMinorState: String, Codable {
+ case abort // failed - user abandoned it
case abortingBank = "aborting-bank" // failed
case acceptRefund = "accept-refund"
case autoRefund = "auto-refund" // TODO: finalizing
@@ -52,6 +53,15 @@ enum TransactionMinorState: String, Codable {
// Placeholder until D37 is fully implemented
case unknown
+ /// wallet-core keeps adding minor states (`abort` was added after this enum was
+ /// written). A raw-value enum throws on an unknown value even when the property is
+ /// Optional, and because the transaction list is decoded as one array that throw
+ /// discards *every* transaction. Degrade to `.unknown` instead.
+ init(from decoder: Decoder) throws {
+ let raw = try decoder.singleValueContainer().decode(String.self)
+ self = TransactionMinorState(rawValue: raw) ?? .unknown
+ }
+
var localizedDbgState: String { self.rawValue }
var localizedState: String? {
switch self {
@@ -623,6 +633,13 @@ enum DenomLossEventType: String, Decodable {
case denomExpired = "denom-expired"
case denomVanished = "denom-vanished"
case denomUnoffered = "denom-unoffered"
+ case denomRevoked = "denom-revoked" // exchange revoked the denomination
+ case unknown // forward compatibility, see TransactionMinorState
+
+ init(from decoder: Decoder) throws {
+ let raw = try decoder.singleValueContainer().decode(String.self)
+ self = DenomLossEventType(rawValue: raw) ?? .unknown
+ }
}
struct DenomLossTransactionDetails: Decodable {
var exchangeBaseUrl: String