taler-ios

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

Model+Exchange.swift (13845B)


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