taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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:
MTalerWallet1/Model/Model+Transactions.swift | 30++++++++++++++++++++++++++++++
MTalerWallet1/Model/Transaction.swift | 17+++++++++++++++++
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