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 }