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