taler-ios

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

Model+Exchange.swift (13414B)


      1 /*
      2  * This file is part of GNU Taler, ©2022-26 Taler Systems S.A.
      3  * See LICENSE.md
      4  */
      5 /**
      6  * Model+Exchange
      7  *
      8  * @author Marc Stibane
      9  */
     10 import Foundation
     11 import taler_swift
     12 import SymLog
     13 
     14 struct OperationErrorInfo: Codable, Hashable {
     15     var error: TalerErrorDetail
     16 }
     17 
     18 enum ExchangeEntryStatus: String, Codable {
     19     case preset
     20     case ephemeral
     21     case used
     22 }
     23 
     24 enum ExchangeUpdateStatus: String, Codable {
     25     case initial
     26     case initialUpdate = "initial-update"
     27     case suspended
     28     case failed
     29     case outdatedUpdate = "outdated-update"
     30     case ready
     31     case readyUpdate = "ready-update"
     32     case unavailable
     33     case unavailableUpdate = "unavailable-update"
     34 
     35     var localized: String {
     36         switch self {
     37             case .initial:           String(localized: "Exchange_status_initial", defaultValue: "Initial")
     38             case .initialUpdate:     String(localized: "Exchange_status_initial_update", defaultValue: "Initial (… updating …)")
     39             case .suspended:         String(localized: "Exchange_status_suspended", defaultValue: "Suspended")
     40             case .failed:            String(localized: "Exchange_status_failed", defaultValue: "Failed")
     41             case .outdatedUpdate:    String(localized: "Exchange_status_outdated_update", defaultValue: "Outdated (… updating …)")
     42             case .ready:             String(localized: "Exchange_status_ready", defaultValue: "Ready")
     43             case .readyUpdate:       String(localized: "Exchange_status_ready_update", defaultValue: "Ready (… updating …)")
     44             case .unavailable:       String(localized: "Exchange_status_unavailable", defaultValue: "Unavailable")
     45             case .unavailableUpdate: String(localized: "Exchange_status_unavailable_update", defaultValue: "Unavailable (… updating …)")
     46         }
     47     }
     48 }
     49 
     50 struct ExchangeState: Codable, Hashable {
     51     var exchangeEntryStatus: ExchangeEntryStatus
     52     var exchangeUpdateStatus: ExchangeUpdateStatus
     53     var tosStatus: ExchangeTosStatus
     54 }
     55 
     56 struct ExchangeTransition: Codable {             // Notification
     57     enum TransitionType: String, Codable {
     58         case transition = "exchange-state-transition"
     59     }
     60     var type: TransitionType
     61     var exchangeBaseUrl: String
     62     var oldExchangeState: ExchangeState
     63     var newExchangeState: ExchangeState
     64 }
     65 
     66 enum BankDialect: Codable, Hashable {
     67     case gls
     68     case unknown(value: String)
     69 
     70     init(from decoder: Decoder) throws {
     71         let container = try decoder.singleValueContainer()
     72         let status = try? container.decode(String.self)
     73         switch status {
     74             case "gls": self = .gls
     75             default:
     76                 self = .unknown(value: status ?? "unknown")
     77         }
     78     }
     79 }
     80 // MARK: -
     81 /// The result from wallet-core's ListExchanges
     82 struct Exchange: Codable, Hashable, Identifiable {
     83     static func < (lhs: Exchange, rhs: Exchange) -> Bool {
     84         let leftScope = lhs.scopeInfo
     85         let rightScope = rhs.scopeInfo
     86         return leftScope < rightScope
     87     }
     88     static func == (lhs: Exchange, rhs: Exchange) -> Bool {
     89         return lhs.exchangeBaseUrl == rhs.exchangeBaseUrl
     90         &&     lhs.tosStatus == rhs.tosStatus
     91 //        &&     lhs.exchangeStatus == rhs.exchangeStatus                         // deprecated
     92         &&     lhs.exchangeEntryStatus == rhs.exchangeEntryStatus
     93         &&     lhs.exchangeUpdateStatus == rhs.exchangeUpdateStatus
     94     }
     95 
     96     var exchangeBaseUrl: String
     97     var masterPub: String?
     98     var scopeInfo: ScopeInfo
     99     var paytoUris: [String]
    100     var tosStatus: ExchangeTosStatus
    101     var exchangeEntryStatus: ExchangeEntryStatus
    102     var exchangeUpdateStatus: ExchangeUpdateStatus
    103     var peerPaymentsDisabled: Bool?
    104     var directDepositsDisabled: Bool?
    105     var noFees: Bool?
    106     var ageRestrictionOptions: [Int]
    107     var bankComplianceLanguage: BankDialect?
    108     var lastUpdateTimestamp: Timestamp?
    109     var lastUpdateErrorInfo: OperationErrorInfo?
    110 
    111 //    walletKycStatus?: ExchangeWalletKycStatus;
    112 //    walletKycReservePub?: string;
    113 //    walletKycAccessToken?: string;
    114 //    walletKycUrl?: string;
    115 
    116     /** Threshold that we've requested to satisfy. */
    117 //    walletKycRequestedThreshold?: string;
    118 
    119     var id: String { exchangeBaseUrl + tosStatus.rawValue }
    120     var name: String? {
    121         if let url = URL(string: exchangeBaseUrl) {
    122             if let host = url.host {
    123                 return host
    124             }
    125         }
    126         return nil
    127     }
    128 }
    129 // MARK: -
    130 struct DefaultExchange: Codable {
    131     var talerUri: String                        // taler://withdraw-exchange/exchange.taler-ops.ch/
    132     var currency: String                        // CHF
    133     var currencySpec: CurrencySpecification     // .name = "Swiss Francs"... alt_unit_names":{"0":"Fr.","-2":"Rp."}
    134 //    var restrictions: [String]                  // ["Swiss bank accounts only"]
    135 }
    136 extension DefaultExchange: Identifiable {
    137     var id: String { talerUri }
    138 
    139 }
    140 // MARK: -
    141 /// A request to list exchanges names for a currency
    142 fileprivate struct ListExchanges: WalletBackendFormattedRequest {
    143     func operation() -> String { "listExchanges" }
    144     func args() -> Args { Args(filterByScope: scope, filterByExchangeEntryStatus: filterByStatus) }
    145 
    146     var scope: ScopeInfo?
    147     var filterByStatus: ExchangeEntryStatus?
    148     struct Args: Encodable {
    149         var filterByScope: ScopeInfo?
    150         var filterByExchangeEntryStatus: ExchangeEntryStatus?
    151     }
    152     struct Response: Decodable {        // list of known exchanges
    153         var exchanges: [Exchange]
    154     }
    155 }
    156 
    157 fileprivate struct DefaultExchanges: WalletBackendFormattedRequest {
    158     func operation() -> String { "getDefaultExchanges" }
    159     func args() -> Args { Args() }
    160 
    161     struct Args: Encodable {}                           // no arguments needed
    162 
    163     struct Response: Decodable {        // list of known exchanges
    164         var defaultExchanges: [DefaultExchange]
    165     }
    166 }
    167 
    168 /// A request to get info for one exchange.
    169 fileprivate struct GetExchangeByUrl: WalletBackendFormattedRequest {
    170     func operation() -> String { "getExchangeEntryByUrl" }
    171     func args() -> Args { Args(exchangeBaseUrl: exchangeBaseUrl) }
    172 
    173     var exchangeBaseUrl: String
    174 
    175     struct Args: Encodable {
    176         var exchangeBaseUrl: String
    177     }
    178     typealias Response = Exchange
    179 }
    180 
    181 /// A request to update a single exchange.
    182 fileprivate struct UpdateExchange: WalletBackendFormattedRequest {
    183     func operation() -> String { "updateExchangeEntry" }
    184 //    func args() -> Args { Args(scopeInfo: scopeInfo) }
    185     func args() -> Args { Args(exchangeBaseUrl: exchangeBaseUrl, force: force) }
    186 
    187 //    var scopeInfo: ScopeInfo
    188     var exchangeBaseUrl: String
    189     var force: Bool
    190     struct Args: Encodable {
    191         var exchangeBaseUrl: String
    192         var force: Bool
    193     }
    194     struct Response: Decodable {}   // no result - getting no error back means success
    195 }
    196 
    197 /// A request to add an exchange.
    198 fileprivate struct AddExchange: WalletBackendFormattedRequest {
    199     func operation() -> String { "addExchange" }
    200     func args() -> Args { Args(uri: uri, allowCompletion: true) }
    201 
    202     var uri: String
    203     struct Args: Encodable {
    204         var uri: String
    205         var allowCompletion: Bool
    206     }
    207     struct Response: Decodable {}   // no result - getting no error back means success
    208 }
    209 
    210 /// A request to delete an exchange.
    211 fileprivate struct DeleteExchange: WalletBackendFormattedRequest {
    212     func operation() -> String { "deleteExchange" }
    213     func args() -> Args { Args(exchangeBaseUrl: exchangeBaseUrl, purge: purge) }
    214 
    215     var exchangeBaseUrl: String
    216     var purge: Bool
    217     struct Args: Encodable {
    218         var exchangeBaseUrl: String
    219         var purge: Bool
    220     }
    221     struct Response: Decodable {}   // no result - getting no error back means success
    222 }
    223 
    224 /// A request to get info about a currency
    225 fileprivate struct GetCurrencySpecification: WalletBackendFormattedRequest {
    226     func operation() -> String { "getCurrencySpecification" }
    227     func args() -> Args { Args(scope: scope) }
    228 
    229     var scope: ScopeInfo
    230     struct Args: Encodable {
    231         var scope: ScopeInfo
    232     }
    233     struct Response: Codable, Sendable {
    234         let currencySpecification: CurrencySpecification
    235     }
    236 }
    237 /// A request to make a currency "global"
    238 fileprivate struct AddGlobalCurrency: WalletBackendFormattedRequest {
    239     func operation() -> String { "addGlobalCurrencyExchange" }
    240     func args() -> Args { Args(currency: currency,
    241                         exchangeBaseUrl: baseUrl,
    242                       exchangeMasterPub: masterPub) }
    243     var currency: String
    244     var baseUrl: String
    245     var masterPub: String
    246     struct Args: Encodable {
    247         var currency: String
    248         var exchangeBaseUrl: String
    249         var exchangeMasterPub: String
    250     }
    251     struct Response: Decodable {}   // no result - getting no error back means success
    252 }
    253 fileprivate struct RmvGlobalCurrency: WalletBackendFormattedRequest {
    254     func operation() -> String { "removeGlobalCurrencyExchange" }
    255     func args() -> Args { Args(currency: currency,
    256                         exchangeBaseUrl: baseUrl,
    257                       exchangeMasterPub: masterPub) }
    258     var currency: String
    259     var baseUrl: String
    260     var masterPub: String
    261     struct Args: Encodable {
    262         var currency: String
    263         var exchangeBaseUrl: String
    264         var exchangeMasterPub: String
    265     }
    266     struct Response: Decodable {}   // no result - getting no error back means success
    267 }
    268 // MARK: -
    269 extension WalletModel {
    270     /// ask wallet-core for its list of known exchanges
    271     nonisolated func listExchanges(scope: ScopeInfo?, filterByStatus: ExchangeEntryStatus? = nil, viewHandles: Bool = false)
    272       async -> [Exchange] {   // M for MainActor
    273         do {
    274             let request = ListExchanges(scope: scope, filterByStatus: filterByStatus)    // .used, .preset
    275             let response = try await sendRequest(request, viewHandles: viewHandles)
    276             return response.exchanges
    277         } catch {
    278             return []               // empty, but not nil
    279         }
    280     }
    281 
    282     /// ask wallet-core for its list of default exchanges   ==> currently only taler-ops.ch
    283     nonisolated func getDefaultExchanges(viewHandles: Bool = false)
    284     async -> [DefaultExchange] {     // M for MainActor
    285         do {
    286             let request = DefaultExchanges()
    287             let response = try await sendRequest(request, viewHandles: viewHandles)
    288             return response.defaultExchanges
    289         } catch {
    290             return []               // empty, but not nil
    291         }
    292     }
    293 
    294     /// add a new exchange with URL to the wallet's list of known exchanges
    295     nonisolated func getExchangeByUrl(url: String, viewHandles: Bool = false)
    296       async throws -> Exchange {
    297         let request = GetExchangeByUrl(exchangeBaseUrl: url)
    298 //            logger.info("query for exchange: \(url, privacy: .public)")
    299         let response = try await sendRequest(request, viewHandles: viewHandles)
    300         return response
    301     }
    302 
    303     /// add a new exchange with URL to the wallet's list of known exchanges
    304     nonisolated func addExchange(uri: String, viewHandles: Bool = false)
    305       async throws {
    306         let request = AddExchange(uri: uri)
    307         logger.info("adding exchange: \(uri, privacy: .public)")
    308         _ = try await sendRequest(request, viewHandles: viewHandles)
    309     }
    310 
    311     /// add a new exchange with URL to the wallet's list of known exchanges
    312     nonisolated func deleteExchange(url: String, purge: Bool = false, viewHandles: Bool = false)
    313       async throws {
    314         let request = DeleteExchange(exchangeBaseUrl: url, purge: purge)
    315         logger.info("deleting exchange: \(url, privacy: .public)")
    316         _ = try await sendRequest(request, viewHandles: viewHandles)
    317     }
    318 
    319     /// ask wallet-core to update an existing exchange by querying it for denominations, fees, and scoped currency info
    320 //    func updateExchange(scopeInfo: ScopeInfo, viewHandles: Bool = false)
    321     nonisolated func updateExchange(exchangeBaseUrl: String, force: Bool = false, viewHandles: Bool = false)
    322       async throws  {
    323         let request = UpdateExchange(exchangeBaseUrl: exchangeBaseUrl, force: force)
    324         logger.info("updating exchange: \(exchangeBaseUrl, privacy: .public)")
    325         _ = try await sendRequest(request, viewHandles: viewHandles)
    326     }
    327 
    328     nonisolated func getCurrencyInfo(scope: ScopeInfo, viewHandles: Bool = false)
    329       async throws -> CurrencyInfo {
    330         let request = GetCurrencySpecification(scope: scope)
    331         let response = try await sendRequest(request, viewHandles: viewHandles)
    332         return CurrencyInfo(specs: response.currencySpecification,
    333                         formatter: CurrencyFormatter.formatter(currency: scope.currency,
    334                                                                   specs: response.currencySpecification))
    335     }
    336 
    337     nonisolated func addGlobalCurrencyExchange(currency: String, baseUrl: String, masterPub: String, viewHandles: Bool = false)
    338       async throws {
    339         let request = AddGlobalCurrency(currency: currency, baseUrl: baseUrl, masterPub: masterPub)
    340         _ = try await sendRequest(request, viewHandles: viewHandles)
    341     }
    342 
    343     nonisolated func rmvGlobalCurrencyExchange(currency: String, baseUrl: String, masterPub: String, viewHandles: Bool = false)
    344       async throws {
    345         let request = RmvGlobalCurrency(currency: currency, baseUrl: baseUrl, masterPub: masterPub)
    346         _ = try await sendRequest(request, viewHandles: viewHandles)
    347     }
    348 }