taler-ios

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

Model+Withdraw.swift (13698B)


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