taler-ios

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

Model+Exchange.swift (11586B)


      1 /*
      2  * This file is part of GNU Taler, ©2022-25 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: "Initial")
     38             case .initialUpdate:     String(localized: "Initial update")
     39             case .suspended:         String(localized: "Suspended")
     40             case .failed:            String(localized: "Failed")
     41             case .outdatedUpdate:    String(localized: "Outdated update")
     42             case .ready:             String(localized: "Ready")
     43             case .readyUpdate:       String(localized: "Ready update")
     44             case .unavailable:       String(localized: "Unavailable")
     45             case .unavailableUpdate: String(localized: "Unavailable update")
     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 noFees: Bool?
    105     var ageRestrictionOptions: [Int]
    106     var bankComplianceLanguage: BankDialect?
    107     var lastUpdateTimestamp: Timestamp?
    108     var lastUpdateErrorInfo: OperationErrorInfo?
    109 
    110 //    walletKycStatus?: ExchangeWalletKycStatus;
    111 //    walletKycReservePub?: string;
    112 //    walletKycAccessToken?: string;
    113 //    walletKycUrl?: string;
    114 
    115     /** Threshold that we've requested to satisfy. */
    116 //    walletKycRequestedThreshold?: string;
    117 
    118     var id: String { exchangeBaseUrl + tosStatus.rawValue }
    119     var name: String? {
    120         if let url = URL(string: exchangeBaseUrl) {
    121             if let host = url.host {
    122                 return host
    123             }
    124         }
    125         return nil
    126     }
    127 }
    128 // MARK: -
    129 /// A request to list exchanges names for a currency
    130 fileprivate struct ListExchanges: WalletBackendFormattedRequest {
    131     func operation() -> String { "listExchanges" }
    132     func args() -> Args { Args(filterByScope: scope, filterByExchangeEntryStatus: filterByStatus) }
    133 
    134     var scope: ScopeInfo?
    135     var filterByStatus: ExchangeEntryStatus?
    136     struct Args: Encodable {
    137         var filterByScope: ScopeInfo?
    138         var filterByExchangeEntryStatus: ExchangeEntryStatus?
    139     }
    140     struct Response: Decodable {        // list of known exchanges
    141         var exchanges: [Exchange]
    142     }
    143 }
    144 
    145 /// A request to get info for one exchange.
    146 fileprivate struct GetExchangeByUrl: WalletBackendFormattedRequest {
    147     func operation() -> String { "getExchangeEntryByUrl" }
    148     func args() -> Args { Args(exchangeBaseUrl: exchangeBaseUrl) }
    149 
    150     var exchangeBaseUrl: String
    151 
    152     struct Args: Encodable {
    153         var exchangeBaseUrl: String
    154     }
    155     typealias Response = Exchange
    156 }
    157 
    158 /// A request to update a single exchange.
    159 fileprivate struct UpdateExchange: WalletBackendFormattedRequest {
    160     func operation() -> String { "updateExchangeEntry" }
    161 //    func args() -> Args { Args(scopeInfo: scopeInfo) }
    162     func args() -> Args { Args(exchangeBaseUrl: exchangeBaseUrl, force: force) }
    163 
    164 //    var scopeInfo: ScopeInfo
    165     var exchangeBaseUrl: String
    166     var force: Bool
    167     struct Args: Encodable {
    168         var exchangeBaseUrl: String
    169         var force: Bool
    170     }
    171     struct Response: Decodable {}   // no result - getting no error back means success
    172 }
    173 
    174 /// A request to add an exchange.
    175 fileprivate struct AddExchange: WalletBackendFormattedRequest {
    176     func operation() -> String { "addExchange" }
    177     func args() -> Args { Args(uri: uri, allowCompletion: true) }
    178 
    179     var uri: String
    180     struct Args: Encodable {
    181         var uri: String
    182         var allowCompletion: Bool
    183     }
    184     struct Response: Decodable {}   // no result - getting no error back means success
    185 }
    186 
    187 /// A request to delete an exchange.
    188 fileprivate struct DeleteExchange: WalletBackendFormattedRequest {
    189     func operation() -> String { "deleteExchange" }
    190     func args() -> Args { Args(exchangeBaseUrl: exchangeBaseUrl, purge: purge) }
    191 
    192     var exchangeBaseUrl: String
    193     var purge: Bool
    194     struct Args: Encodable {
    195         var exchangeBaseUrl: String
    196         var purge: Bool
    197     }
    198     struct Response: Decodable {}   // no result - getting no error back means success
    199 }
    200 
    201 /// A request to get info about a currency
    202 fileprivate struct GetCurrencySpecification: WalletBackendFormattedRequest {
    203     func operation() -> String { "getCurrencySpecification" }
    204     func args() -> Args { Args(scope: scope) }
    205 
    206     var scope: ScopeInfo
    207     struct Args: Encodable {
    208         var scope: ScopeInfo
    209     }
    210     struct Response: Codable, Sendable {
    211         let currencySpecification: CurrencySpecification
    212     }
    213 }
    214 /// A request to make a currency "global"
    215 fileprivate struct AddGlobalCurrency: WalletBackendFormattedRequest {
    216     func operation() -> String { "addGlobalCurrencyExchange" }
    217     func args() -> Args { Args(currency: currency,
    218                         exchangeBaseUrl: baseUrl,
    219                       exchangeMasterPub: masterPub) }
    220     var currency: String
    221     var baseUrl: String
    222     var masterPub: String
    223     struct Args: Encodable {
    224         var currency: String
    225         var exchangeBaseUrl: String
    226         var exchangeMasterPub: String
    227     }
    228     struct Response: Decodable {}   // no result - getting no error back means success
    229 }
    230 fileprivate struct RmvGlobalCurrency: WalletBackendFormattedRequest {
    231     func operation() -> String { "removeGlobalCurrencyExchange" }
    232     func args() -> Args { Args(currency: currency,
    233                         exchangeBaseUrl: baseUrl,
    234                       exchangeMasterPub: masterPub) }
    235     var currency: String
    236     var baseUrl: String
    237     var masterPub: String
    238     struct Args: Encodable {
    239         var currency: String
    240         var exchangeBaseUrl: String
    241         var exchangeMasterPub: String
    242     }
    243     struct Response: Decodable {}   // no result - getting no error back means success
    244 }
    245 // MARK: -
    246 extension WalletModel {
    247     /// ask wallet-core for its list of known exchanges
    248     nonisolated func listExchanges(scope: ScopeInfo?, filterByStatus: ExchangeEntryStatus? = nil, viewHandles: Bool = false)
    249       async -> [Exchange] {   // M for MainActor
    250         do {
    251             let request = ListExchanges(scope: scope, filterByStatus: filterByStatus)    // .used, .preset
    252             let response = try await sendRequest(request, viewHandles: viewHandles)
    253             return response.exchanges
    254         } catch {
    255             return []               // empty, but not nil
    256         }
    257     }
    258 
    259     /// add a new exchange with URL to the wallet's list of known exchanges
    260     nonisolated func getExchangeByUrl(url: String, viewHandles: Bool = false)
    261       async throws -> Exchange {
    262         let request = GetExchangeByUrl(exchangeBaseUrl: url)
    263 //            logger.info("query for exchange: \(url, privacy: .public)")
    264         let response = try await sendRequest(request, viewHandles: viewHandles)
    265         return response
    266     }
    267 
    268     /// add a new exchange with URL to the wallet's list of known exchanges
    269     nonisolated func addExchange(uri: String, viewHandles: Bool = false)
    270       async throws {
    271         let request = AddExchange(uri: uri)
    272         logger.info("adding exchange: \(uri, privacy: .public)")
    273         _ = try await sendRequest(request, viewHandles: viewHandles)
    274     }
    275 
    276     /// add a new exchange with URL to the wallet's list of known exchanges
    277     nonisolated func deleteExchange(url: String, purge: Bool = false, viewHandles: Bool = false)
    278       async throws {
    279         let request = DeleteExchange(exchangeBaseUrl: url, purge: purge)
    280         logger.info("deleting exchange: \(url, privacy: .public)")
    281         _ = try await sendRequest(request, viewHandles: viewHandles)
    282     }
    283 
    284     /// ask wallet-core to update an existing exchange by querying it for denominations, fees, and scoped currency info
    285 //    func updateExchange(scopeInfo: ScopeInfo, viewHandles: Bool = false)
    286     nonisolated func updateExchange(exchangeBaseUrl: String, force: Bool = false, viewHandles: Bool = false)
    287       async throws  {
    288         let request = UpdateExchange(exchangeBaseUrl: exchangeBaseUrl, force: force)
    289         logger.info("updating exchange: \(exchangeBaseUrl, privacy: .public)")
    290         _ = try await sendRequest(request, viewHandles: viewHandles)
    291     }
    292 
    293     nonisolated func getCurrencyInfo(scope: ScopeInfo, viewHandles: Bool = false)
    294       async throws -> CurrencyInfo {
    295         let request = GetCurrencySpecification(scope: scope)
    296         let response = try await sendRequest(request, viewHandles: viewHandles)
    297         return CurrencyInfo(specs: response.currencySpecification,
    298                         formatter: CurrencyFormatter.formatter(currency: scope.currency,
    299                                                                   specs: response.currencySpecification))
    300     }
    301 
    302     nonisolated func addGlobalCurrencyExchange(currency: String, baseUrl: String, masterPub: String, viewHandles: Bool = false)
    303       async throws {
    304         let request = AddGlobalCurrency(currency: currency, baseUrl: baseUrl, masterPub: masterPub)
    305         _ = try await sendRequest(request, viewHandles: viewHandles)
    306     }
    307 
    308     nonisolated func rmvGlobalCurrencyExchange(currency: String, baseUrl: String, masterPub: String, viewHandles: Bool = false)
    309       async throws {
    310         let request = RmvGlobalCurrency(currency: currency, baseUrl: baseUrl, masterPub: masterPub)
    311         _ = try await sendRequest(request, viewHandles: viewHandles)
    312     }
    313 }