taler-ios

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

Model+Withdraw.swift (14328B)


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