taler-ios

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

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 }