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 }