commit fdc36ed73a352738674a14cc9daf5f263050edf7
parent d89d45bd527e441c68706392cac5faa0d9ffde52
Author: Marc Stibane <marc@taler.net>
Date: Wed, 19 Aug 2026 07:54:59 +0200
AI: prepare Exchange master-key rotation
Diffstat:
2 files changed, 37 insertions(+), 0 deletions(-)
diff --git a/TalerWallet1/Model/Model+Exchange.swift b/TalerWallet1/Model/Model+Exchange.swift
@@ -58,6 +58,18 @@ struct ExchangeState: Codable, Hashable {
var tosStatus: ExchangeTosStatus
}
+/// Set on an Exchange when it changed its master key and the user has not
+/// confirmed the change yet. Withdrawals are refused while this is present
+/// (wallet-core throws WALLET_EXCHANGE_KEYS_NOT_ACCEPTED), until the app calls
+/// confirmExchangeKeyChange().
+struct ExchangeKeyChangeInfo: Codable, Hashable {
+ var currentMasterPub: String
+ var currentCurrency: String
+ var supersededMasterPub: String
+ var supersededCurrency: String
+ var sharesDenominations: Bool
+}
+
struct ExchangeTransition: Codable { // Notification
var type: String // exchange-state-transition
var exchangeBaseUrl: String
@@ -109,6 +121,7 @@ struct Exchange: Codable, Hashable, Identifiable {
var bankComplianceLanguage: BankDialect?
var lastUpdateTimestamp: Timestamp?
var lastUpdateErrorInfo: OperationErrorInfo?
+ var unconfirmedKeyChange: ExchangeKeyChangeInfo? // if set, withdrawals are refused until confirmed
// walletKycStatus?: ExchangeWalletKycStatus;
// walletKycReservePub?: string;
@@ -232,6 +245,21 @@ fileprivate struct DeleteExchange: WalletBackendFormattedRequest {
struct Response: Decodable {} // no result - getting no error back means success
}
+/// A request to confirm that a changed exchange key set is legitimate.
+/// Needed before a withdrawal can proceed once `unconfirmedKeyChange` is set.
+fileprivate struct ConfirmExchangeKeyChange: WalletBackendFormattedRequest {
+ var operation: String { "confirmExchangeKeyChange" }
+ func args() -> Args { Args(exchangeBaseUrl: exchangeBaseUrl, currentMasterPub: currentMasterPub) }
+
+ var exchangeBaseUrl: String
+ var currentMasterPub: String
+ struct Args: Encodable {
+ var exchangeBaseUrl: String
+ var currentMasterPub: String
+ }
+ struct Response: Decodable {} // no result - getting no error back means success
+}
+
/// A request to get info about a currency
fileprivate struct GetCurrencySpecification: WalletBackendFormattedRequest {
var operation: String { "getCurrencySpecification" }
@@ -342,6 +370,14 @@ extension WalletModel {
_ = try await sendRequest(request, viewHandles: viewHandles)
}
+ /// confirm a changed exchange master key so withdrawals are allowed again
+ nonisolated func confirmExchangeKeyChange(exchangeBaseUrl: String, currentMasterPub: String, viewHandles: Bool = false)
+ async throws {
+ let request = ConfirmExchangeKeyChange(exchangeBaseUrl: exchangeBaseUrl, currentMasterPub: currentMasterPub)
+ logger.info("confirming exchange key change: \(exchangeBaseUrl, privacy: .public)")
+ _ = try await sendRequest(request, viewHandles: viewHandles)
+ }
+
nonisolated func getCurrencyInfo(scope: ScopeInfo, viewHandles: Bool = false)
async throws -> CurrencyInfo {
let request = GetCurrencySpecification(scope: scope)
diff --git a/TalerWallet1/Model/Model+Withdraw.swift b/TalerWallet1/Model/Model+Withdraw.swift
@@ -140,6 +140,7 @@ struct WithdrawalDetailsForAmount: Decodable {
var withdrawalAccountsList: AccountDetailsArray?
var ageRestrictionOptions: [Int]? // Array of ages
var scopeInfo: ScopeInfo
+ var unconfirmedKeyChange: ExchangeKeyChangeInfo? // if set, accepting the withdrawal will fail until confirmed
}
/// A request to get an exchange's withdrawal details.
fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest {