Model+Withdraw.swift (14328B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import Foundation 9 import taler_swift 10 import SymLog 11 12 enum AccountRestrictionType: String, Codable { 13 case deny 14 case regex 15 } 16 17 struct AccountRestriction: Codable, Hashable { 18 var type: AccountRestrictionType 19 var payto_regex: String? 20 var human_hint: String? 21 var human_hint_i18n: I18nDict? 22 } 23 extension AccountRestriction: Identifiable { 24 var id: AccountRestriction {self} 25 } 26 27 enum TransferType: String, Codable { 28 case payto 29 case chQrBill = "ch-qr-bill" 30 case uri 31 } 32 33 struct QrCodeSpec: Codable, Hashable { 34 var type: String 35 var qrContent: String 36 } 37 38 struct TransferOption: Codable, Hashable { 39 var type: TransferType 40 var uri: String? // only if type == uri 41 var paytoUri: String? // not if type == uri 42 var qrCodes: [QrCodeSpec]? // not if type == uri 43 var qrReferenceNumber: String? // only if type == chQrBill 44 } 45 46 struct ExchangeAccountDetails: Codable, Hashable { 47 var status: String // "OK" or "error" - then conversionError 48 // var paytoUri: String // deprecated, use transferOptions 49 var transferAmount: Amount? // only if "OK" 50 var transferOptions: [TransferOption]? 51 var transferExpiry: Timestamp? 52 var bankLabel: String? // only if wallet-core knows it 53 var currencySpecification: CurrencySpecification? // only if wallet-core knows it 54 var creditRestrictions: [AccountRestriction]? // only if restrictions apply 55 // var conversionError: TalerErrorDetail? // only if error 56 var scope: ScopeInfo? // not sent by wallet-core, set by withScope() 57 } 58 59 typealias AccountDetailsArray = [ExchangeAccountDetails] 60 61 extension AccountDetailsArray { 62 /// filter array from Exchange for "ok" 63 var validDetails: AccountDetailsArray? { 64 let result = self.filter { detail in 65 detail.status.lowercased() == "ok" 66 } 67 if !result.isEmpty { 68 return result // guaranteed to have at least 1 item 69 } 70 return nil 71 } 72 73 /// wallet-core's WithdrawalExchangeAccountDetails has no scope, and currencySpecification 74 /// only for conversion accounts. Take the transaction's scope for every account whose 75 /// transferAmount is in that currency, so formatted(specs:scope:) needn't fall back to "1.00 EUR" 76 func withScope(_ scope: ScopeInfo) -> AccountDetailsArray { 77 self.map { detail in 78 var detail = detail 79 if detail.scope == nil && detail.transferAmount?.currencyStr == scope.currency { 80 detail.scope = scope 81 } 82 return detail 83 } 84 } 85 } 86 87 // MARK: - 88 enum WithdrawalOperationStatus: String, Codable { 89 case pending 90 case selected 91 case aborted 92 case confirmed 93 } 94 /// The result from prepareBankIntegratedWithdrawal 95 struct WithdrawUriInfoResponse: Decodable { 96 var operationId: String 97 var status: WithdrawalOperationStatus // pending, selected, aborted, confirmed 98 var confirmTransferUrl: String? 99 var currency: String // use this if amount=nil 100 var amount: Amount? // if nil then either ask User (editableAmount=true), or it's cash2ecash (editableAmount=false) 101 var editableAmount: Bool // if true then ask User 102 var maxAmount: Amount? // limit how much the user may withdraw 103 var wireFee: Amount? 104 var defaultExchangeBaseUrl: String? // if nil then use possibleExchanges 105 var editableExchange: Bool // TODO: what for? 106 var possibleExchanges: [Exchange] // TODO: query these for fees? 107 } 108 109 struct PrepareBankIntegratedWithdrawalResponse: Decodable { 110 var transactionId: String 111 var info: WithdrawUriInfoResponse 112 } 113 114 /// A request to get an exchange's withdrawal details. 115 fileprivate struct PrepareBankIntegratedWithdrawal: WalletBackendFormattedRequest { 116 typealias Response = PrepareBankIntegratedWithdrawalResponse 117 var operation: String { "prepareBankIntegratedWithdrawal" } 118 func args() -> Args { Args(talerWithdrawUri: talerUri, 119 progressToken: talerUri) } // TODO: lastPathComponent 120 var talerUri: String 121 struct Args: Encodable { 122 var talerWithdrawUri: String 123 var progressToken: String? 124 } 125 } 126 // MARK: - 127 /// The result from prepareWithdrawExchange 128 struct WithdrawExchangeResponse: Decodable { 129 var exchangeBaseUrl: String 130 var amount: Amount? 131 } 132 /// A request to get an exchange's withdrawal details. 133 fileprivate struct PrepareWithdrawExchange: WalletBackendFormattedRequest { 134 typealias Response = WithdrawExchangeResponse 135 var operation: String { "prepareWithdrawExchange" } 136 func args() -> Args { Args(talerUri: talerUri, 137 progressToken: talerUri) } 138 var talerUri: String 139 struct Args: Encodable { 140 var talerUri: String 141 var progressToken: String? 142 } 143 } 144 // MARK: - 145 /// The result from getWithdrawalDetailsForAmount 146 struct WithdrawalDetailsForAmount: Decodable { 147 var exchangeBaseUrl: String 148 var amountRaw: Amount // Amount that the user will transfer to the exchange 149 var amountEffective: Amount // Amount that will be added to the user's wallet balance 150 var numCoins: Int? // Number of coins this amountEffective will create 151 var withdrawalAccountsList: AccountDetailsArray? 152 var ageRestrictionOptions: [Int]? // Array of ages 153 var scopeInfo: ScopeInfo 154 var unconfirmedKeyChange: ExchangeKeyChangeInfo? // if set, accepting the withdrawal will fail until confirmed 155 } 156 /// A request to get an exchange's withdrawal details. 157 fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest { 158 typealias Response = WithdrawalDetailsForAmount 159 var operation: String { "getWithdrawalDetailsForAmount" } 160 func args() -> Args { Args(amount: amount, 161 exchangeBaseUrl: baseUrl, 162 restrictScope: scope, 163 progressToken: baseUrl) } 164 var amount: Amount 165 var baseUrl: String? 166 var scope: ScopeInfo? 167 struct Args: Encodable { 168 var amount: Amount 169 var exchangeBaseUrl: String? // needed for b-i-withdrawals 170 var restrictScope: ScopeInfo? // only if exchangeBaseUrl is nil 171 var progressToken: String? 172 } 173 } 174 // MARK: - 175 enum ExchangeTosStatus: String, Codable { 176 case missingTos = "missing-tos" 177 case pending 178 case proposed 179 case accepted 180 } 181 struct ExchangeTermsOfService: Decodable { 182 var currentEtag: String 183 var acceptedEtag: String? 184 var tosStatus: ExchangeTosStatus 185 var tosAvailableLanguages: [String] 186 var contentType: String 187 var contentLanguage: String? 188 var content: String 189 } 190 /// A request to query an exchange's terms of service. 191 fileprivate struct GetExchangeTermsOfService: WalletBackendFormattedRequest { 192 typealias Response = ExchangeTermsOfService 193 var operation: String { "getExchangeTos" } 194 func args() -> Args { Args(exchangeBaseUrl: baseUrl, 195 acceptedFormat: acceptedFormat, 196 acceptLanguage: acceptLanguage, 197 progressToken: baseUrl) } 198 var baseUrl: String 199 var acceptedFormat: [String]? 200 var acceptLanguage: String? 201 struct Args: Encodable { 202 var exchangeBaseUrl: String 203 var acceptedFormat: [String]? 204 var acceptLanguage: String? 205 var progressToken: String 206 } 207 } 208 /// A request to mark an exchange's terms of service as accepted. 209 fileprivate struct SetExchangeTOSAccepted: WalletBackendFormattedRequest { 210 struct Response: Decodable {} // no result - getting no error back means success 211 var operation: String { "setExchangeTosAccepted" } 212 func args() -> Args { Args(exchangeBaseUrl: baseUrl) } 213 214 var baseUrl: String 215 216 struct Args: Encodable { 217 var exchangeBaseUrl: String 218 } 219 } 220 // MARK: - 221 struct AcceptWithdrawalResponse: Decodable { 222 var transactionId: String 223 var confirmTransferUrl: String? 224 // var reservePub: String 225 } 226 /// A request to accept a bank-integrated withdrawl. 227 fileprivate struct AcceptBankIntegratedWithdrawal: WalletBackendFormattedRequest { 228 typealias Response = AcceptWithdrawalResponse 229 var operation: String { "acceptBankIntegratedWithdrawal" } 230 func args() -> Args { Args(talerWithdrawUri: talerUri, exchangeBaseUrl: baseUrl, 231 amount: amount, restrictAge: restrictAge, progressToken: talerUri) } 232 233 var talerUri: String 234 var baseUrl: String 235 var amount: Amount? 236 var restrictAge: Int? 237 238 struct Args: Encodable { 239 var talerWithdrawUri: String 240 var exchangeBaseUrl: String 241 var amount: Amount? 242 var restrictAge: Int? 243 var progressToken: String 244 } 245 } 246 // MARK: - 247 struct AcceptManualWithdrawalResult: Decodable { 248 var reservePub: String 249 var withdrawalAccountsList: AccountDetailsArray 250 var transactionId: String 251 } 252 /// A request to accept a manual withdrawl. 253 fileprivate struct AcceptManualWithdrawal: WalletBackendFormattedRequest { 254 typealias Response = AcceptManualWithdrawalResult 255 var operation: String { "acceptManualWithdrawal" } 256 func args() -> Args { Args(amount: amount, exchangeBaseUrl: baseUrl, restrictAge: restrictAge) } 257 258 var amount: Amount 259 var baseUrl: String 260 var restrictAge: Int? 261 262 struct Args: Encodable { 263 var amount: Amount 264 var exchangeBaseUrl: String 265 var restrictAge: Int? 266 } 267 } 268 // MARK: - 269 extension WalletModel { 270 /// load withdraw-exchange details. Networking involved 271 nonisolated func prepareWithdrawExchange(_ talerUri: String, 272 viewHandles: Bool = false) 273 async throws -> WithdrawExchangeResponse { 274 let request = PrepareWithdrawExchange(talerUri: talerUri) 275 let controller = Controller.shared 276 controller.progressOperation = request.operation 277 controller.progressToken = talerUri 278 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 279 return response 280 } 281 282 /// load withdrawal details. Networking involved 283 nonisolated func prepareBankIntegratedWithdrawal(_ talerUri: String, 284 viewHandles: Bool = false) 285 async throws -> PrepareBankIntegratedWithdrawalResponse { 286 let request = PrepareBankIntegratedWithdrawal(talerUri: talerUri) 287 let controller = Controller.shared 288 controller.progressOperation = request.operation 289 controller.progressToken = talerUri 290 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 291 return response 292 } 293 294 nonisolated func getWithdrawalDetailsForAmount(_ amount: Amount, 295 baseUrl: String?, 296 scope: ScopeInfo?, 297 viewHandles: Bool = false) 298 async throws -> WithdrawalDetailsForAmount { 299 let request = GetWithdrawalDetailsForAmount(amount: amount, baseUrl: baseUrl, scope: scope) 300 let controller = Controller.shared 301 controller.progressOperation = request.operation 302 controller.progressToken = baseUrl 303 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 304 return response 305 } 306 307 nonisolated func getExchangeTos(_ baseUrl: String, 308 acceptedFormat: [String], 309 acceptLanguage: String, 310 viewHandles: Bool = false) 311 async throws -> ExchangeTermsOfService { 312 let request = GetExchangeTermsOfService(baseUrl: baseUrl, 313 acceptedFormat: acceptedFormat, 314 acceptLanguage: acceptLanguage) 315 let controller = Controller.shared 316 controller.progressOperation = request.operation 317 controller.progressToken = baseUrl 318 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 319 return response 320 } 321 322 nonisolated func setExchangeTOSAccepted(_ baseUrl: String, 323 viewHandles: Bool = false) 324 async throws -> Decodable { 325 let request = SetExchangeTOSAccepted(baseUrl: baseUrl) 326 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 327 return response 328 } 329 330 nonisolated func acceptBankIntWithdrawal(_ baseUrl: String, 331 withdrawURL: String, 332 amount: Amount?, 333 restrictAge: Int?, 334 viewHandles: Bool = false) 335 async throws -> AcceptWithdrawalResponse? { 336 let request = AcceptBankIntegratedWithdrawal(talerUri: withdrawURL, baseUrl: baseUrl, 337 amount: amount, restrictAge: restrictAge) 338 let controller = Controller.shared 339 controller.progressOperation = request.operation 340 controller.progressToken = withdrawURL 341 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 342 return response 343 } 344 345 nonisolated func acceptManualWithdrawal(_ amount: Amount, 346 baseUrl: String, 347 restrictAge: Int?, 348 viewHandles: Bool = false) 349 async throws -> AcceptManualWithdrawalResult? { 350 let request = AcceptManualWithdrawal(amount: amount, baseUrl: baseUrl, restrictAge: restrictAge) 351 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 352 return response 353 } 354 }