taler-ios

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

Model+Deposit.swift (13039B)


      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                           clientCancellationId: "cancel") }
    189     var depositPaytoUri: String
    190     var amount: Amount
    191     struct Args: Encodable {
    192         var depositPaytoUri: String
    193         var amount: Amount
    194         var clientCancellationId: 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 response = try await sendRequest(request, viewHandles: viewHandles)
    203         return response
    204     }
    205 }
    206 // MARK: -
    207 struct DepositGroupResult: Decodable {
    208     var transactionId: String
    209     var txState: TransactionState?
    210 }
    211 /// A request to deposit some coins.
    212 fileprivate struct CreateDepositGroup: WalletBackendFormattedRequest {
    213     typealias Response = DepositGroupResult
    214     var operation: String { "createDepositGroup" }
    215     func args() -> Args { Args(depositPaytoUri: depositPaytoUri,
    216                                  restrictScope: scope,
    217                                         amount: amount,
    218                                  progressToken: depositPaytoUri) }
    219     var depositPaytoUri: String
    220     var scope: ScopeInfo
    221     var amount: Amount
    222     struct Args: Encodable {
    223         var depositPaytoUri: String
    224         var restrictScope: ScopeInfo
    225         var amount: Amount
    226         var progressToken: String
    227     }
    228 }
    229 extension WalletModel {
    230     /// deposit coins. Networking involved
    231     nonisolated func createDepositGroup(_ depositPaytoUri: String, scope: ScopeInfo, amount: Amount, viewHandles: Bool = false)
    232       async throws -> DepositGroupResult {
    233         let request = CreateDepositGroup(depositPaytoUri: depositPaytoUri, scope: scope, amount: amount)
    234         let controller = Controller.shared
    235         controller.progressOperation = request.operation
    236         controller.progressToken = depositPaytoUri
    237         let response = try await sendRequest(request, viewHandles: viewHandles)
    238         return response
    239     }
    240 }
    241 // MARK: -
    242 struct BankAccountsInfo: Decodable, Hashable {
    243     var bankAccountId: String
    244     var paytoUri: String
    245     var kycCompleted: Bool
    246     var currencies: [String]?       // Currencies supported by the bank, if known
    247     var label: String?
    248     var payToWorkAround: String {
    249         let payto = PayTo(paytoUri)
    250         
    251         if let cyclos = payto.cyclos {
    252             if cyclos.count > 1 {
    253                 // TODO: wallet-core should ensure that receivers are valid and don't have whitespace
    254                 // https://bugs.gnunet.org/view.php?id=11726
    255                 let receiver = payto.receiver?.replacingOccurrences(of: SPACE, with: "+") ?? DEMO
    256                 return String("payto://cyclos/\(cyclos)?receiver-name=\(receiver)")
    257             }
    258         }
    259         return paytoUri
    260     }
    261 }
    262 struct BankAccounts: Decodable, Hashable {
    263     var accounts: [BankAccountsInfo]
    264 }
    265 /// A request to list known bank accounts.
    266 fileprivate struct ListBankAccounts: WalletBackendFormattedRequest {
    267     typealias Response = BankAccounts
    268     var operation: String { "listBankAccounts" }
    269     func args() -> Args { Args(currency: currency) }
    270     var currency: String?
    271     struct Args: Encodable {
    272         var currency: String?
    273     }
    274 }
    275 extension WalletModel {
    276     /// ask for known accounts. No networking
    277     nonisolated func listBankAccounts(_ currency: String? = nil, viewHandles: Bool = false)
    278       async throws -> [BankAccountsInfo] {
    279         let request = ListBankAccounts(currency: currency)
    280         let response = try await sendRequest(request, viewHandles: viewHandles)
    281         return response.accounts
    282     }
    283 }
    284 
    285 /// A request to get one bank account.
    286 fileprivate struct GetBankAccountById: WalletBackendFormattedRequest {
    287     typealias Response = BankAccountsInfo
    288     var operation: String { "getBankAccountById" }
    289     func args() -> Args { Args(bankAccountId: bankAccountId) }
    290     var bankAccountId: String
    291     struct Args: Encodable {
    292         var bankAccountId: String
    293     }
    294 }
    295 extension WalletModel {
    296     /// ask for a specific account. No networking
    297     nonisolated func getBankAccountById(_ bankAccountId: String, viewHandles: Bool = false)
    298       async throws -> BankAccountsInfo {
    299         let request = GetBankAccountById(bankAccountId: bankAccountId)
    300         let response = try await sendRequest(request, viewHandles: viewHandles)
    301         return response
    302     }
    303 }
    304 
    305 struct AddBankAccountResponse: Decodable, Hashable {
    306     var bankAccountId: String                       // Identifier of the added bank account
    307 }
    308 /// A request to add a known bank account.
    309 fileprivate struct AddBankAccount: WalletBackendFormattedRequest {
    310     typealias Response = AddBankAccountResponse
    311     var operation: String { "addBankAccount" }
    312     func args() -> Args { Args(paytoUri: uri,
    313                                   label: label,
    314                    replaceBankAccountId: replace) }
    315     var uri: String
    316     var label: String
    317     var replace: String?
    318     struct Args: Encodable {
    319         var paytoUri: String                        // bank account that should be added
    320         var label: String                           // Human-readable label
    321         var replaceBankAccountId: String?                // account that this new account should replace
    322     }
    323 }
    324 extension WalletModel {
    325     /// add (or update) a known account. No networking
    326     nonisolated func addBankAccount(_ uri: String,
    327                                     label: String,
    328                                   replace: String? = nil,
    329                               viewHandles: Bool = false)
    330       async throws -> String {
    331         let request = AddBankAccount(uri: uri, label: label, replace: replace)
    332         let response = try await sendRequest(request, viewHandles: viewHandles)
    333         return response.bankAccountId
    334     }
    335 }
    336 
    337 /// A request to forget a known bank account.
    338 fileprivate struct ForgetBankAccount: WalletBackendFormattedRequest {
    339     struct Response: Decodable {}   // no result - getting no error back means success
    340     var operation: String { "forgetBankAccount" }
    341     func args() -> Args { Args(bankAccountId: accountId) }
    342     var accountId: String
    343     struct Args: Encodable {
    344         var bankAccountId: String                        // bank account that should be forgotten
    345     }
    346 }
    347 extension WalletModel {
    348     /// add a known account. No networking
    349     nonisolated func forgetBankAccount(_ accountId: String, viewHandles: Bool = false)
    350       async throws {
    351         let request = ForgetBankAccount(accountId: accountId)
    352         _ = try await sendRequest(request, viewHandles: viewHandles)
    353     }
    354 }