Model+Withdraw.swift (13445B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 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 } 144 /// A request to get an exchange's withdrawal details. 145 fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest { 146 typealias Response = WithdrawalDetailsForAmount 147 var operation: String { "getWithdrawalDetailsForAmount" } 148 func args() -> Args { Args(amount: amount, 149 exchangeBaseUrl: baseUrl, 150 restrictScope: scope, 151 clientCancellationId: "cancel") } 152 var amount: Amount 153 var baseUrl: String? 154 var scope: ScopeInfo? 155 struct Args: Encodable { 156 var amount: Amount 157 var exchangeBaseUrl: String? // needed for b-i-withdrawals 158 var restrictScope: ScopeInfo? // only if exchangeBaseUrl is nil 159 var clientCancellationId: String? 160 } 161 } 162 // MARK: - 163 enum ExchangeTosStatus: String, Codable { 164 case missingTos = "missing-tos" 165 case pending 166 case proposed 167 case accepted 168 } 169 struct ExchangeTermsOfService: Decodable { 170 var currentEtag: String 171 var acceptedEtag: String? 172 var tosStatus: ExchangeTosStatus 173 var tosAvailableLanguages: [String] 174 var contentType: String 175 var contentLanguage: String? 176 var content: String 177 } 178 /// A request to query an exchange's terms of service. 179 fileprivate struct GetExchangeTermsOfService: WalletBackendFormattedRequest { 180 typealias Response = ExchangeTermsOfService 181 var operation: String { "getExchangeTos" } 182 func args() -> Args { Args(exchangeBaseUrl: baseUrl, 183 acceptedFormat: acceptedFormat, 184 acceptLanguage: acceptLanguage, 185 progressToken: baseUrl) } 186 var baseUrl: String 187 var acceptedFormat: [String]? 188 var acceptLanguage: String? 189 struct Args: Encodable { 190 var exchangeBaseUrl: String 191 var acceptedFormat: [String]? 192 var acceptLanguage: String? 193 var progressToken: String 194 } 195 } 196 /// A request to mark an exchange's terms of service as accepted. 197 fileprivate struct SetExchangeTOSAccepted: WalletBackendFormattedRequest { 198 struct Response: Decodable {} // no result - getting no error back means success 199 var operation: String { "setExchangeTosAccepted" } 200 func args() -> Args { Args(exchangeBaseUrl: baseUrl) } 201 202 var baseUrl: String 203 204 struct Args: Encodable { 205 var exchangeBaseUrl: String 206 } 207 } 208 // MARK: - 209 struct AcceptWithdrawalResponse: Decodable { 210 var transactionId: String 211 var confirmTransferUrl: String? 212 // var reservePub: String 213 } 214 /// A request to accept a bank-integrated withdrawl. 215 fileprivate struct AcceptBankIntegratedWithdrawal: WalletBackendFormattedRequest { 216 typealias Response = AcceptWithdrawalResponse 217 var operation: String { "acceptBankIntegratedWithdrawal" } 218 func args() -> Args { Args(talerWithdrawUri: talerUri, exchangeBaseUrl: baseUrl, 219 amount: amount, restrictAge: restrictAge, progressToken: talerUri) } 220 221 var talerUri: String 222 var baseUrl: String 223 var amount: Amount? 224 var restrictAge: Int? 225 226 struct Args: Encodable { 227 var talerWithdrawUri: String 228 var exchangeBaseUrl: String 229 var amount: Amount? 230 var restrictAge: Int? 231 var progressToken: String 232 } 233 } 234 // MARK: - 235 struct AcceptManualWithdrawalResult: Decodable { 236 var reservePub: String 237 var withdrawalAccountsList: AccountDetailsArray 238 var transactionId: String 239 } 240 /// A request to accept a manual withdrawl. 241 fileprivate struct AcceptManualWithdrawal: WalletBackendFormattedRequest { 242 typealias Response = AcceptManualWithdrawalResult 243 var operation: String { "acceptManualWithdrawal" } 244 func args() -> Args { Args(amount: amount, exchangeBaseUrl: baseUrl, restrictAge: restrictAge) } 245 246 var amount: Amount 247 var baseUrl: String 248 var restrictAge: Int? 249 250 struct Args: Encodable { 251 var amount: Amount 252 var exchangeBaseUrl: String 253 var restrictAge: Int? 254 } 255 } 256 // MARK: - 257 extension WalletModel { 258 /// load withdraw-exchange details. Networking involved 259 nonisolated func prepareWithdrawExchange(_ talerUri: String, 260 viewHandles: Bool = false) 261 async throws -> WithdrawExchangeResponse { 262 let request = PrepareWithdrawExchange(talerUri: talerUri) 263 let controller = Controller.shared 264 controller.progressOperation = request.operation 265 controller.progressToken = talerUri 266 let response = try await sendRequest(request, viewHandles: viewHandles) 267 return response 268 } 269 270 /// load withdrawal details. Networking involved 271 nonisolated func prepareBankIntegratedWithdrawal(_ talerUri: String, 272 viewHandles: Bool = false) 273 async throws -> PrepareBankIntegratedWithdrawalResponse { 274 let request = PrepareBankIntegratedWithdrawal(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 nonisolated func getWithdrawalDetailsForAmount(_ amount: Amount, 283 baseUrl: String?, 284 scope: ScopeInfo?, 285 viewHandles: Bool = false) 286 async throws -> WithdrawalDetailsForAmount { 287 let request = GetWithdrawalDetailsForAmount(amount: amount, baseUrl: baseUrl, scope: scope) 288 let response = try await sendRequest(request, viewHandles: viewHandles) 289 return response 290 } 291 292 nonisolated func getExchangeTos(_ baseUrl: String, 293 acceptedFormat: [String], 294 acceptLanguage: String, 295 viewHandles: Bool = false) 296 async throws -> ExchangeTermsOfService { 297 let request = GetExchangeTermsOfService(baseUrl: baseUrl, 298 acceptedFormat: acceptedFormat, 299 acceptLanguage: acceptLanguage) 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 setExchangeTOSAccepted(_ baseUrl: String, 308 viewHandles: Bool = false) 309 async throws -> Decodable { 310 let request = SetExchangeTOSAccepted(baseUrl: baseUrl) 311 let response = try await sendRequest(request, viewHandles: viewHandles) 312 return response 313 } 314 315 nonisolated func acceptBankIntWithdrawal(_ baseUrl: String, 316 withdrawURL: String, 317 amount: Amount?, 318 restrictAge: Int?, 319 viewHandles: Bool = false) 320 async throws -> AcceptWithdrawalResponse? { 321 let request = AcceptBankIntegratedWithdrawal(talerUri: withdrawURL, baseUrl: baseUrl, 322 amount: amount, restrictAge: restrictAge) 323 let controller = Controller.shared 324 controller.progressOperation = request.operation 325 controller.progressToken = withdrawURL 326 let response = try await sendRequest(request, viewHandles: viewHandles) 327 return response 328 } 329 330 nonisolated func acceptManualWithdrawal(_ amount: Amount, 331 baseUrl: String, 332 restrictAge: Int?, 333 viewHandles: Bool = false) 334 async throws -> AcceptManualWithdrawalResult? { 335 let request = AcceptManualWithdrawal(amount: amount, baseUrl: baseUrl, restrictAge: restrictAge) 336 let response = try await sendRequest(request, viewHandles: viewHandles) 337 return response 338 } 339 }