taler-ios

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

Model+Exchange.swift (13764B)


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