taler-ios

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

Model+Deposit.swift (13190B)


      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 AnyCodable
     11 //import SymLog
     12 
     13 struct AccountChange: Codable {             // Notification
     14     enum TransitionType: String, Codable {
     15         case change = "bank-account-change"
     16     }
     17     var type: TransitionType
     18     var bankAccountId: String
     19 }
     20 
     21 // MARK: - IBAN
     22 struct ValidateIbanResult: Codable {
     23     let valid: Bool
     24 }
     25 /// A request to validate an IBAN.
     26 fileprivate struct ValidateIban: WalletBackendFormattedRequest {
     27     typealias Response = ValidateIbanResult
     28     var operation: String { "validateIban" }
     29     func args() -> Args { Args(iban: iban) }
     30 
     31     var iban: String
     32     struct Args: Encodable {
     33         var iban: String
     34     }
     35 }
     36 extension WalletModel {
     37     /// validate IBAN. No Networking
     38     nonisolated func validateIban(_ iban: String, viewHandles: Bool = false)
     39       async throws -> Bool {
     40         let request = ValidateIban(iban: iban)
     41         let response = try await sendRequest(request, viewHandles: viewHandles)
     42         return response.valid
     43     }
     44 }
     45 // MARK: - Deposit
     46 enum PaytoType: String, Codable {
     47     case unknown
     48     case iban       // or BBAN for HUF (and CHF when taler://dev-experiment/fake-chf-bban was called)
     49     case bitcoin
     50     case cyclos
     51     case xTalerBank = "x-taler-bank"
     52 }
     53 
     54 struct IbanAccountFieldToPaytoResponse: Codable {
     55     let ok: Bool
     56     let type: String?       // either "iban" or "bban", only if ok==true
     57     let paytoUri: String?   // only if ok==true
     58 }
     59 
     60 /// A request to convert an IBAN/BBAN to PayTo.
     61 fileprivate struct IbanAccountFieldToPayto: WalletBackendFormattedRequest {
     62     typealias Response = IbanAccountFieldToPaytoResponse
     63     var operation: String { "convertIbanAccountFieldToPayto" }
     64     func args() -> Args { Args(value: value, currency: currency) }
     65 
     66     var value: String
     67     var currency: String
     68     struct Args: Encodable {
     69         var value: String
     70         var currency: String
     71     }
     72 }
     73 
     74 struct IbanPaytoToAccountFieldResponse: Codable {
     75     let type: String
     76     let value: String
     77 }
     78 
     79 /// A request to convert PayTo to IBAN/BBAN.
     80 fileprivate struct IbanPaytoToAccountField: WalletBackendFormattedRequest {
     81     typealias Response = IbanPaytoToAccountFieldResponse
     82     var operation: String { "convertIbanPaytoToAccountField" }
     83     func args() -> Args { Args(paytoUri: paytoUri) }
     84 
     85     var paytoUri: String
     86     struct Args: Encodable {
     87         var paytoUri: String
     88     }
     89 }
     90 
     91 extension WalletModel {
     92     /// convert an IBAN/BBAN to PayTo
     93     nonisolated func convertIbanAccountFieldToPayto(_ value: String, currency: String, viewHandles: Bool = false)
     94       async throws -> IbanAccountFieldToPaytoResponse {
     95         let request = IbanAccountFieldToPayto(value: value, currency: currency)
     96         let response = try await sendRequest(request, viewHandles: viewHandles)
     97         return response
     98     }
     99     nonisolated func convertIbanPaytoToAccountField(_ paytoUri: String, viewHandles: Bool = false)
    100       async throws -> IbanPaytoToAccountFieldResponse {
    101         let request = IbanPaytoToAccountField(paytoUri: paytoUri)
    102         let response = try await sendRequest(request, viewHandles: viewHandles)
    103         return response
    104     }
    105 }
    106 // MARK: - Deposit
    107 struct WireTypeDetails: Codable {
    108     let paymentTargetType: PaytoType
    109     let preferredEntryType: String?         // iban / bban  TODO: use this
    110     let talerBankHostnames: [String]?
    111 }
    112 
    113 struct DepositWireTypesResponse: Codable {
    114     /// can be used to pre-filter payment target types to offer the user as an input option
    115 //    let wireTypes: [PaytoType]
    116     let wireTypeDetails: [WireTypeDetails]
    117 }
    118 
    119 /// A request to get wire types that can be used for a deposit operation.
    120 fileprivate struct DepositWireTypes: WalletBackendFormattedRequest {
    121     typealias Response = DepositWireTypesResponse
    122     var operation: String { "getDepositWireTypes" }
    123     func args() -> Args { Args(currency: currency, scopeInfo: scopeInfo) }
    124 
    125     var currency: String?
    126     var scopeInfo: ScopeInfo?
    127     struct Args: Encodable {
    128         // one of these must be set - don't set both to nil
    129         var currency: String?
    130         var scopeInfo: ScopeInfo?
    131     }
    132 }
    133 extension WalletModel {
    134     /// Get wire types that can be used for a deposit operation
    135     nonisolated func depositWireTypes(_ currency: String?, scopeInfo: ScopeInfo? = nil, viewHandles: Bool = false)
    136       async throws -> [WireTypeDetails] {
    137         let request = DepositWireTypes(currency: currency, scopeInfo: scopeInfo)
    138         let response = try await sendRequest(request, viewHandles: viewHandles)
    139         return response.wireTypeDetails
    140     }
    141 }
    142 // MARK: - max Deposit amount
    143 /// Check if initiating a deposit is possible, check fees
    144 fileprivate struct AmountResponse: Codable {
    145     let effectiveAmount: Amount?
    146     let rawAmount: Amount
    147 }
    148 fileprivate struct GetMaxDepositAmount: WalletBackendFormattedRequest {
    149     typealias Response = AmountResponse
    150     var operation: String { "getMaxDepositAmount" }
    151     func args() -> Args { Args(currency: scope.currency, restrictScope: scope) }
    152 
    153     var scope: ScopeInfo
    154     struct Args: Encodable {
    155         var currency: String
    156         var restrictScope: ScopeInfo
    157 //        var depositPaytoUri: String
    158     }
    159 }
    160 extension WalletModel {
    161     nonisolated func getMaxDepositAmount(_ scope: ScopeInfo, viewHandles: Bool = false)
    162       async throws -> Amount {
    163         let request = GetMaxDepositAmount(scope: scope)
    164         let response = try await sendRequest(request, viewHandles: viewHandles)
    165         return response.rawAmount
    166     }
    167 } // getMaxDepositAmount
    168 // MARK: - Deposit
    169 struct DepositFees: Codable {
    170     let coin: Amount
    171     let wire: Amount
    172     let refresh: Amount
    173 }
    174 struct CheckDepositResponse: Codable {
    175     let totalDepositCost: Amount
    176     let effectiveDepositAmount: Amount
    177     let fees: DepositFees
    178     let kycSoftLimit: Amount?
    179     let kycHardLimit: Amount?
    180     let kycExchanges: [String]?                 // Base URL of exchanges that would likely require soft KYC
    181 }
    182 /// A request to get an exchange's deposit contract terms.
    183 fileprivate struct CheckDeposit: WalletBackendFormattedRequest {
    184     typealias Response = CheckDepositResponse
    185     var operation: String { "checkDeposit" }
    186     func args() -> Args { Args(depositPaytoUri: depositPaytoUri,
    187                                         amount: amount,
    188                                  progressToken: depositPaytoUri) }
    189     var depositPaytoUri: String
    190     var amount: Amount
    191     struct Args: Encodable {
    192         var depositPaytoUri: String
    193         var amount: Amount
    194         var progressToken: String?
    195     }
    196 }
    197 extension WalletModel {
    198     /// check fees for deposit. No Networking
    199     nonisolated func checkDeposit4711(_ depositPaytoUri: String, amount: Amount, viewHandles: Bool = false)
    200       async throws -> CheckDepositResponse {
    201         let request = CheckDeposit(depositPaytoUri: depositPaytoUri, amount: amount)
    202         let controller = Controller.shared
    203         controller.progressOperation = request.operation
    204         controller.progressToken = depositPaytoUri
    205         let response = try await sendRequest(request, viewHandles: viewHandles)
    206         return response
    207     }
    208 }
    209 // MARK: -
    210 struct DepositGroupResult: Decodable {
    211     var transactionId: String
    212     var txState: TransactionState?
    213 }
    214 /// A request to deposit some coins.
    215 fileprivate struct CreateDepositGroup: WalletBackendFormattedRequest {
    216     typealias Response = DepositGroupResult
    217     var operation: String { "createDepositGroup" }
    218     func args() -> Args { Args(depositPaytoUri: depositPaytoUri,
    219                                  restrictScope: scope,
    220                                         amount: amount,
    221                                  progressToken: depositPaytoUri) }
    222     var depositPaytoUri: String
    223     var scope: ScopeInfo
    224     var amount: Amount
    225     struct Args: Encodable {
    226         var depositPaytoUri: String
    227         var restrictScope: ScopeInfo
    228         var amount: Amount
    229         var progressToken: String
    230     }
    231 }
    232 extension WalletModel {
    233     /// deposit coins. Networking involved
    234     nonisolated func createDepositGroup(_ depositPaytoUri: String, scope: ScopeInfo, amount: Amount, viewHandles: Bool = false)
    235       async throws -> DepositGroupResult {
    236         let request = CreateDepositGroup(depositPaytoUri: depositPaytoUri, scope: scope, amount: amount)
    237         let controller = Controller.shared
    238         controller.progressOperation = request.operation
    239         controller.progressToken = depositPaytoUri
    240         let response = try await sendRequest(request, viewHandles: viewHandles)
    241         return response
    242     }
    243 }
    244 // MARK: -
    245 struct BankAccountsInfo: Decodable, Hashable {
    246     var bankAccountId: String
    247     var paytoUri: String
    248     var kycCompleted: Bool
    249     var currencies: [String]?       // Currencies supported by the bank, if known
    250     var label: String?
    251     var payToWorkAround: String {
    252         let payto = PayTo(paytoUri)
    253         
    254         if let cyclos = payto.cyclos {
    255             if cyclos.count > 1 {
    256                 // TODO: wallet-core should ensure that receivers are valid and don't have whitespace
    257                 // https://bugs.gnunet.org/view.php?id=11726
    258                 let receiver = payto.receiver?.replacingOccurrences(of: SPACE, with: "+") ?? DEMO
    259                 return String("payto://cyclos/\(cyclos)?receiver-name=\(receiver)")
    260             }
    261         }
    262         return paytoUri
    263     }
    264 }
    265 struct BankAccounts: Decodable, Hashable {
    266     var accounts: [BankAccountsInfo]
    267 }
    268 /// A request to list known bank accounts.
    269 fileprivate struct ListBankAccounts: WalletBackendFormattedRequest {
    270     typealias Response = BankAccounts
    271     var operation: String { "listBankAccounts" }
    272     func args() -> Args { Args(currency: currency) }
    273     var currency: String?
    274     struct Args: Encodable {
    275         var currency: String?
    276     }
    277 }
    278 extension WalletModel {
    279     /// ask for known accounts. No networking
    280     nonisolated func listBankAccounts(_ currency: String? = nil, viewHandles: Bool = false)
    281       async throws -> [BankAccountsInfo] {
    282         let request = ListBankAccounts(currency: currency)
    283         let response = try await sendRequest(request, viewHandles: viewHandles)
    284         return response.accounts
    285     }
    286 }
    287 
    288 /// A request to get one bank account.
    289 fileprivate struct GetBankAccountById: WalletBackendFormattedRequest {
    290     typealias Response = BankAccountsInfo
    291     var operation: String { "getBankAccountById" }
    292     func args() -> Args { Args(bankAccountId: bankAccountId) }
    293     var bankAccountId: String
    294     struct Args: Encodable {
    295         var bankAccountId: String
    296     }
    297 }
    298 extension WalletModel {
    299     /// ask for a specific account. No networking
    300     nonisolated func getBankAccountById(_ bankAccountId: String, viewHandles: Bool = false)
    301       async throws -> BankAccountsInfo {
    302         let request = GetBankAccountById(bankAccountId: bankAccountId)
    303         let response = try await sendRequest(request, viewHandles: viewHandles)
    304         return response
    305     }
    306 }
    307 
    308 struct AddBankAccountResponse: Decodable, Hashable {
    309     var bankAccountId: String                       // Identifier of the added bank account
    310 }
    311 /// A request to add a known bank account.
    312 fileprivate struct AddBankAccount: WalletBackendFormattedRequest {
    313     typealias Response = AddBankAccountResponse
    314     var operation: String { "addBankAccount" }
    315     func args() -> Args { Args(paytoUri: uri,
    316                                   label: label,
    317                    replaceBankAccountId: replace) }
    318     var uri: String
    319     var label: String
    320     var replace: String?
    321     struct Args: Encodable {
    322         var paytoUri: String                        // bank account that should be added
    323         var label: String                           // Human-readable label
    324         var replaceBankAccountId: String?                // account that this new account should replace
    325     }
    326 }
    327 extension WalletModel {
    328     /// add (or update) a known account. No networking
    329     nonisolated func addBankAccount(_ uri: String,
    330                                     label: String,
    331                                   replace: String? = nil,
    332                               viewHandles: Bool = false)
    333       async throws -> String {
    334         let request = AddBankAccount(uri: uri, label: label, replace: replace)
    335         let response = try await sendRequest(request, viewHandles: viewHandles)
    336         return response.bankAccountId
    337     }
    338 }
    339 
    340 /// A request to forget a known bank account.
    341 fileprivate struct ForgetBankAccount: WalletBackendFormattedRequest {
    342     struct Response: Decodable {}   // no result - getting no error back means success
    343     var operation: String { "forgetBankAccount" }
    344     func args() -> Args { Args(bankAccountId: accountId) }
    345     var accountId: String
    346     struct Args: Encodable {
    347         var bankAccountId: String                        // bank account that should be forgotten
    348     }
    349 }
    350 extension WalletModel {
    351     /// add a known account. No networking
    352     nonisolated func forgetBankAccount(_ accountId: String, viewHandles: Bool = false)
    353       async throws {
    354         let request = ForgetBankAccount(accountId: accountId)
    355         _ = try await sendRequest(request, viewHandles: viewHandles)
    356     }
    357 }