taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }