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