taler-ios

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

Model+Payment.swift (23173B)


      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 typealias I18nDict = [String: String]           // two-char language code, e.g. "de", "en"
     14 // MARK: - ContractTerms
     15 
     16 struct TokenIssuePublicKey: Codable {
     17     let cipher: String          // "RSA", "CS"
     18 
     19     // RSA public key
     20     let rsaPub: String?         // RSA public key converted to Crockford Base32
     21 
     22     // CS public key
     23     let csPub: String?          // 32-byte value representing a point on Curve25519
     24 
     25     // Start time of this key's signatures validity period
     26     let signatureValidityStart: Timestamp
     27 
     28     // End time of this key's signatures validity period
     29     let signatureValidityEnd: Timestamp
     30 
     31     enum CodingKeys: String, CodingKey {
     32         case cipher
     33         case rsaPub = "rsa_pub"
     34         case csPub = "cs_pub"
     35         case signatureValidityStart = "signature_validity_start"
     36         case signatureValidityEnd = "signature_validity_end"
     37     }
     38 }
     39 
     40 struct ContractTokenDetails: Codable {
     41     let clazz: String                           // "subscription", "discount"
     42 
     43     // Array of domain names where this subscription can be safely used
     44     // (e.g. the issuer warrants that these sites will re-issue tokens of this type
     45     // if the respective contract says so).  May contain "*" for any domain or subdomain.
     46     let trustedDomains: [String]?               // only for subscription
     47 
     48     // Array of domain names where this discount token is intended to be used.
     49     // May contain "*" for any domain or subdomain.  Users should be warned about sites
     50     // proposing to consume discount tokens of this type that are not in this list that
     51     // the merchant is accepting a coupon from a competitor and thus may be attaching
     52     // different semantics (like get 20% discount for my competitors 30% discount token).
     53     let expectedDomains: [String]?              // only for discount
     54 
     55     enum CodingKeys: String, CodingKey {
     56         case clazz = "class"
     57         case trustedDomains = "trusted_domains"
     58         case expectedDomains = "expected_domains"
     59     }
     60 }
     61 
     62 struct ContractTokenFamily: Codable {
     63     // Human-readable name of the token family.
     64     let name: String
     65 
     66     // Human-readable description of the semantics of this token family (for display).
     67     let description: String
     68 
     69     // Map from IETF BCP 47 language tags to localized descriptions.
     70     let descriptionI18n: I18nDict?
     71 
     72     // Public keys used to validate tokens issued by this token family.
     73     let keys: [TokenIssuePublicKey]
     74 
     75     // Kind-specific information of the token
     76     let details: ContractTokenDetails
     77 
     78     // Must a wallet understand this token type to
     79     // process contracts that use or issue it?
     80     let critical: Bool
     81 
     82     enum CodingKeys: String, CodingKey {
     83         case name, description
     84         case descriptionI18n = "description_i18n"
     85         case keys, details, critical
     86     }
     87 }
     88 
     89 struct ContractInput: Codable, Hashable {
     90     let type: String                            // "token"
     91 
     92     // Slug of the token family in the token_families map on the order
     93     let tokenFamilySlug: String?
     94 
     95     // Number of tokens of this type required.
     96     // Defaults to one if the field is not provided.
     97     let count: Int?
     98 
     99     enum CodingKeys: String, CodingKey {
    100         case type, count
    101         case tokenFamilySlug = "token_family_slug"
    102     }
    103 }
    104 
    105 struct ContractOutput: Codable, Hashable {
    106     let type: String                            // "token"
    107 
    108     // Slug of the token family in the token_families map on the order
    109     let tokenFamilySlug: String?
    110 
    111     // Number of tokens of this type required.
    112     // Defaults to one if the field is not provided.
    113     let count: Int?
    114 
    115     // Index of the public key for this output token
    116     // in the ContractTokenFamily keys array.
    117     let keyIndex: Int
    118 
    119     enum CodingKeys: String, CodingKey {
    120         case type, count
    121         case tokenFamilySlug = "token_family_slug"
    122         case keyIndex = "key_index"
    123     }
    124 }
    125 
    126 struct ContractOutputTaxReceipt: Codable {
    127     let type: String                            // "tax-receipt"
    128 
    129     // Array of base URLs of donation authorities that can be
    130     // used to issue the tax receipts. The client must select one.
    131     let donauUrls: [String]
    132 
    133     // Total amount that will be on the tax receipt.
    134     let amount: Amount
    135 
    136     enum CodingKeys: String, CodingKey {
    137         case type, amount
    138         case donauUrls = "donau_urls"
    139     }
    140 }
    141 
    142 struct ContractChoice: Codable, Hashable {
    143     let amount: Amount                          // Total amount payable
    144     let maxFee: Amount                          // Maximum deposit fee covered by the merchant
    145     let description: String?                    //
    146     let descriptionI18n: I18nDict?              //      "      localized     "
    147     let inputs: [ContractInput]
    148     let outputs: [ContractOutput]
    149 
    150     enum CodingKeys: String, CodingKey {
    151         case amount, description
    152         case maxFee = "max_fee"
    153         case descriptionI18n = "description_i18n"
    154         case inputs, outputs
    155     }
    156 
    157     var descI18n: String? {
    158         if let i18nDict = self.descriptionI18n {
    159             if !i18nDict.isEmpty {
    160                 for code in Locale.preferredLanguageCodes {
    161                     if let descI18n = i18nDict[code] {
    162                         return descI18n
    163                     }
    164                 }
    165             }
    166         }
    167         if let desc = self.description {
    168             return desc
    169         }
    170         return nil
    171     }
    172 }
    173 
    174 struct MerchantContractTerms: Codable {
    175     let version: Int?                   // v0 doesn't know this
    176 
    177     // ContractTermsV0
    178     let amount: Amount?                 // Total amount payable
    179     let maxFee: Amount?                 // Maximum deposit fee covered by the merchant
    180 
    181     // ContractTermsCommon
    182     let summary: String                 // Human-readable short summary of the contract
    183     let summaryI18n: I18nDict?          //      "      localized     "
    184     let orderID: String                 // uniquely identify the purchase within one merchant instance
    185     let publicReorderURL: String?       // URL meant to share the shopping cart
    186     let fulfillmentURL: String?         // Fulfillment URL to view the product or delivery status
    187     let fulfillmentMessage: String?     // Plain text fulfillment message in the merchant's default language
    188     let fulfillmentMessageI18n: String? // Plain text fulfillment message in the merchant's default language
    189     let products: [Product]?            // Products that are sold in this contract
    190     let timestamp: Timestamp            // Time when the contract was generated by the merchant
    191     let refundDeadline: Timestamp?      // Deadline for refunds
    192     let payDeadline: Timestamp          // Deadline to pay for the contract
    193     let wireTransferDeadline: Timestamp?// Deadline for the wire transfer
    194     let merchantPub: String             // Public key of the merchant
    195     let merchantBaseURL: String         // Base URL of the merchant's backend
    196     let merchant: MerchantInfo
    197     let hWire: String                   // Hash of the merchant's wire details
    198     let wireMethod: String              // merchant wants to use
    199     let exchanges: [ExchangeForPay]
    200     let deliveryLocation: Location?     // Delivery location for (all!) products
    201     let deliveryDate: Timestamp?        // indicating when the order should be delivered
    202     let nonce: String                   // used to ensure freshness
    203     let autoRefund: Duration?
    204     let extra: Extra?                   // Extra data, interpreted by the merchant only
    205     let minimumAge: Int?
    206 
    207     let defaultMoneyPot: Int?
    208 
    209 // deprecated   let auditors: [Auditor]?
    210 
    211     // ContractTermsV1
    212     let choices: [ContractChoice]?
    213     // Map of storing metadata and issue keys of
    214     // token families referenced in this contract.
    215     // @since protocol **vSUBSCRIBE**
    216     let tokenFamilies: [String: ContractTokenFamily]?   // token_family_slug: String
    217 
    218     enum CodingKeys: String, CodingKey {
    219         case version
    220         case amount
    221         case maxFee = "max_fee"
    222 
    223         case summary
    224         case summaryI18n = "summary_i18n"
    225         case orderID = "order_id"
    226         case publicReorderURL = "public_reorder_url"
    227         case fulfillmentURL = "fulfillment_url"
    228         case fulfillmentMessage = "fulfillment_message"
    229         case fulfillmentMessageI18n = "fulfillment_message_i18n"
    230         case products
    231         case timestamp
    232         case refundDeadline = "refund_deadline"
    233         case payDeadline = "pay_deadline"
    234         case wireTransferDeadline = "wire_transfer_deadline"
    235         case merchantPub = "merchant_pub"
    236         case merchantBaseURL = "merchant_base_url"
    237         case merchant
    238         case hWire = "h_wire"
    239         case wireMethod = "wire_method"
    240         case exchanges
    241         case deliveryLocation = "delivery_location"
    242         case deliveryDate = "delivery_date"
    243         case nonce
    244         case autoRefund = "auto_refund"
    245         case extra
    246         case minimumAge = "minimum_age"
    247         case defaultMoneyPot = "default_money_pot"
    248 
    249 //        case auditors
    250         case choices
    251         case tokenFamilies = "token_families"
    252     }
    253 }
    254 // MARK: - Auditor
    255 struct Auditor: Codable {
    256     let name: String
    257     let auditorPub: String
    258     let url: String
    259 
    260     enum CodingKeys: String, CodingKey {
    261         case name
    262         case auditorPub = "auditor_pub"
    263         case url
    264     }
    265 }
    266 // MARK: - Exchange
    267 struct ExchangeForPay: Codable {
    268     let url: String
    269     let masterPub: String
    270 
    271     enum CodingKeys: String, CodingKey {
    272         case url
    273         case masterPub = "master_pub"
    274     }
    275 }
    276 // MARK: - Extra
    277 struct Extra: Codable {
    278     let articleName: String?
    279 
    280     enum CodingKeys: String, CodingKey {
    281         case articleName = "article_name"
    282     }
    283 }
    284 // MARK: -
    285 enum PreparePayResultType: String, Codable {
    286     case paymentPossible = "payment-possible"
    287     case alreadyConfirmed = "already-confirmed"
    288     case insufficientBalance = "insufficient-balance"
    289     case choiceSelection = "choice-selection"
    290 }
    291 
    292 struct ExchangeFeeGapEstimate: Codable {
    293     let balanceAvailable: Amount
    294     let balanceMaterial: Amount
    295     let balanceExchangeDepositable: Amount
    296     let balanceAgeAcceptable: Amount
    297     let balanceReceiverAcceptable: Amount
    298     let balanceReceiverDepositable: Amount
    299     let maxEffectiveSpendAmount: Amount
    300 }
    301 
    302 struct PerScopeDetails: Codable {
    303     let scopeInfo: ScopeInfo
    304 }
    305 
    306 /// The result from PreparePayForUri2 and preparePayForTemplate2
    307 struct PreparePayResult2: Codable {
    308     let transactionId: String
    309 }
    310 /// A request to get an exchange's payment contract terms.
    311 fileprivate struct PreparePayForUri: WalletBackendFormattedRequest {
    312     typealias Response = PreparePayResult2
    313     func operation() -> String { "preparePayForUriV2" }
    314     func args() -> Args { Args(talerPayUri: talerPayUri) }
    315 
    316     var talerPayUri: String
    317     struct Args: Encodable {
    318         var talerPayUri: String
    319     }
    320 }
    321 
    322 /**
    323  * Forced coin selection for deposits/payments.
    324  */
    325 struct ValueContribution: Codable {
    326     var value: Amount
    327     var contribution: Amount
    328 }
    329 struct ForcedCoinSel: Codable {
    330     var coins: [ValueContribution]
    331 }
    332 
    333 enum ChoiceSelectionDetailStatus: String, Codable {
    334     case paymentPossible = "payment-possible"
    335     case insufficientBalance = "insufficient-balance"
    336 }
    337 
    338 enum TokenAvailabilityHint: String, Codable {
    339     case walletTokensAvailableInsufficient = "wallet-tokens-available-insufficient"
    340     case merchantUnexpected = "merchant-unexpected"
    341     case merchantUntrusted = "merchant-untrusted"
    342 
    343 }
    344 struct TokenFamily: Codable, Hashable, Equatable {
    345     var causeHint: TokenAvailabilityHint?
    346     var requested: Int
    347     var available: Int
    348     var unexpected: Int
    349     var untrusted: Int
    350 }
    351 
    352 struct TokenDetails: Codable, Hashable, Equatable {
    353     var tokensRequested: Int
    354     var tokensAvailable: Int
    355     var tokensUnexpected: Int
    356     var tokensUntrusted: Int
    357     var perTokenFamily: [String: TokenFamily]
    358 }
    359 
    360 struct ChoiceSelectionDetail: Codable, Hashable, Sendable {
    361     var status: ChoiceSelectionDetailStatus
    362     var amountRaw: Amount
    363     var scopeInfo: ScopeInfo?                                   // only if wallet-core has the info
    364     var amountEffective: Amount?                                // only if possible
    365     var tokenDetails: TokenDetails?                             // only if possible
    366     var balanceDetails: PaymentInsufficientBalanceDetails?      // only if insufficient
    367 }
    368 
    369 typealias ChoiceTriple = (ChoiceSelectionDetail, ContractChoice, Int)
    370 
    371 struct ChoicesForPayment: Codable {
    372     var choices: [ChoiceSelectionDetail]
    373     /**
    374      * Index of the choice in @e choices array to present to the user as default.
    375      * Won´t be set if no default selection is configured or no choice is payable,
    376      * otherwise, it will always be 0 for v0 orders.
    377      */
    378     var defaultChoiceIndex: Int?
    379     /**
    380      * Whether the choice referenced by @e automaticExecutableIndex
    381      * should be confirmed automatically without user interaction.
    382      *
    383      * If true, the wallet should call `confirmPay' immediately afterwards
    384      * If false, the user should be first prompted to select and confirm a choice.
    385      * Undefined when no choices are payable.
    386      */
    387     var automaticExecution: Bool?
    388     var automaticExecutableIndex: Int?
    389 
    390     var contractTerms: MerchantContractTerms
    391 
    392     func choiceTriple() -> ([ChoiceTriple], Bool)? {
    393         let terms = self.contractTerms
    394         if let ctChoices = terms.choices {
    395             let combined = Array(zip(choices, ctChoices, ctChoices.indices))
    396             return (combined, true)
    397         } else if let amount = terms.amount {       // V0
    398             let maxFee = terms.maxFee ?? Amount.zero(currency: amount.currencyStr)
    399             let ctChoice = ContractChoice(amount: amount,
    400                                           maxFee: maxFee,
    401                                      description: terms.summary,
    402                                  descriptionI18n: terms.summaryI18n,
    403                                           inputs: [],
    404                                          outputs: [])
    405             let combined = Array(zip(choices, [ctChoice], [0]))
    406             return (combined, false)
    407         }
    408 //        symLog.log("  ❗️Yikes, neither choices nor amount in contractTerms!\n\(stack)")
    409         return nil
    410     }
    411 }
    412 
    413 /// A request to get an exchange's payment contract terms.
    414 fileprivate struct GetChoicesForPayment: WalletBackendFormattedRequest {
    415     typealias Response = ChoicesForPayment
    416     func operation() -> String { "getChoicesForPayment" }
    417     func args() -> Args { Args(transactionId: transactionId, forcedCoinSel: forcedCoinSel) }
    418 
    419     var transactionId: String
    420     var forcedCoinSel: ForcedCoinSel?
    421     struct Args: Encodable {
    422         var transactionId: String
    423         var forcedCoinSel: ForcedCoinSel?
    424     }
    425 }
    426 
    427 struct TemplateParams: Codable {
    428     let amount: Amount?                     // Total amount payable
    429     let summary: String?                    // Human-readable short summary of the contract
    430 }
    431 /// A request to get an exchange's payment contract terms.
    432 fileprivate struct PreparePayForTemplateRequest: WalletBackendFormattedRequest {
    433     typealias Response = PreparePayResult2
    434     func operation() -> String { "preparePayForTemplateV2" }
    435     func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams) }
    436 
    437     var talerPayTemplateUri: String
    438     var templateParams: TemplateParams
    439     struct Args: Encodable {
    440         var talerPayTemplateUri: String
    441         var templateParams: TemplateParams
    442     }
    443 }
    444 // MARK: -
    445 struct TemplateContractDetails: Codable {
    446     let summary: String?                // Human-readable short summary of the contract. Editable if nil
    447     let currency: String?               // specify currency when amount is nil - unspecified if nil
    448     let amount: Amount?                 // Total amount payable. Fixed if this field exists, editable if nil
    449     let scopeInfo: ScopeInfo?
    450     let minimumAge: Int?
    451     let payDuration: Duration?
    452     let maxPickupDuration: Duration?
    453     let websiteRegex: String?
    454     let choices: [OrderChoice]?
    455     let templateType: String?
    456 
    457     enum CodingKeys: String, CodingKey {
    458         case summary, currency, amount
    459         case scopeInfo, choices
    460         case minimumAge = "minimum_age"
    461         case payDuration = "pay_duration"
    462         case maxPickupDuration = "max_pickup_duration"
    463         case websiteRegex = "website_regex"
    464         case templateType = "template_type"
    465     }
    466 }
    467 
    468 struct OrderChoice: Codable {
    469     let amount: Amount
    470     let tip: Amount?
    471     let description: String?
    472     let descriptionI18n: I18nDict?
    473     let inputs: [OrderInput]?
    474     let outputs: [OrderOutput]?     // TODO: OrderOutputTaxReceipt
    475     let maxFee: Amount?
    476 
    477     enum CodingKeys: String, CodingKey {
    478         case amount, tip, description
    479         case descriptionI18n = "description_i18n"
    480         case inputs, outputs
    481         case maxFee = "max_fee"
    482     }
    483 }
    484 
    485 struct OrderInput: Codable {    // see ContractInput
    486     let type: String                            // "token"
    487 
    488     // Token family slug as configured in the merchant backend.
    489     // Slug is unique across all configured tokens of a merchant.
    490     let tokenFamilySlug: String?
    491 
    492     // How many units of the input are required.
    493     // Defaults to 1 if not specified.
    494     // Output with count == 0 are ignored by the merchant backend.
    495     let count: Int?
    496 
    497     enum CodingKeys: String, CodingKey {
    498         case type, count
    499         case tokenFamilySlug = "token_family_slug"
    500     }
    501 }
    502 
    503 struct OrderOutput: Codable {   // TODO: ContractOutput
    504     let type: String                            // "token"
    505 
    506     // Token family slug as configured in the merchant backend.
    507     // Slug is unique across all configured tokens of a merchant.
    508     let tokenFamilySlug: String?
    509 
    510     // How many units of the output are issued by the merchant.
    511     // Defaults to 1 if not specified.
    512     // Output with count == 0 are ignored by the merchant backend.
    513     let count: Int?
    514 
    515     // When should the output token be valid. Can be specified if the
    516     // desired validity period should be in the future (like selling
    517     // a subscription for the next month). Optional. If not given,
    518     // the validity is supposed to be "now" (time of order creation).
    519     let validAt: Timestamp?
    520 
    521     enum CodingKeys: String, CodingKey {
    522         case type, count
    523         case tokenFamilySlug = "token_family_slug"
    524         case validAt = "valid_at"
    525     }
    526 }
    527 struct OrderOutputTaxReceipt: Codable {
    528     let type: String                            // "tax-receipt"
    529 }
    530 
    531 struct TemplateContractDetailsDefaults: Codable {
    532     let summary: String?                // Default 'Human-readable summary' when editable: empty if nil
    533     let currency: String?               // Default currency when unspecified: any if nil (e.g. donations)
    534     let amount: Amount?                 // Default amount when editable: unspecified if nil
    535 }
    536 struct TalerMerchantTemplateDetails: Codable {
    537     let templateContract: TemplateContractDetails
    538     let editableDefaults: TemplateContractDetailsDefaults?
    539 //    let requiredCurrency: String?
    540     enum CodingKeys: String, CodingKey {
    541         case templateContract = "template_contract"
    542         case editableDefaults = "editable_defaults"
    543 //        case requiredCurrency = "required_currency"
    544     }
    545 }
    546 
    547 /// The result from checkPayForTemplate
    548 struct WalletTemplateDetails: Codable {
    549     let templateDetails: TalerMerchantTemplateDetails
    550     let supportedCurrencies: [String]
    551 }
    552 /// A request to get an exchange's payment contract terms.
    553 fileprivate struct CheckPayForTemplate: WalletBackendFormattedRequest {
    554     typealias Response = WalletTemplateDetails
    555     func operation() -> String { "checkPayForTemplate" }
    556     func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri) }
    557 
    558     var talerPayTemplateUri: String
    559     struct Args: Encodable {
    560         var talerPayTemplateUri: String
    561     }
    562 }
    563 // MARK: -
    564 /// The result from confirmPayForUri
    565 struct ConfirmPayResult: Decodable {
    566     var type: String                                // done || pending
    567     var contractTerms: MerchantContractTerms?       // only if type==done
    568     var transactionId: String
    569     var lastError: TalerErrorDetail?                // might, but only if type==pending
    570 }
    571 /// A request to get an exchange's payment details.
    572 fileprivate struct ConfirmPayForUri: WalletBackendFormattedRequest {
    573     typealias Response = ConfirmPayResult
    574     func operation() -> String { "confirmPay" }
    575     func args() -> Args { Args(transactionId: transactionId, noWait: true,
    576                                  choiceIndex: choiceIndex) }
    577     var transactionId: String
    578     var choiceIndex: Int?
    579     struct Args: Encodable {
    580         var transactionId: String
    581         var noWait: Bool?
    582         var useDonau: Bool?
    583         var sessionId: String?
    584         var forcedCoinSel: ForcedCoinSel?
    585         /**
    586          * Whether token selection should be forced
    587          * e.g. use tokens with non-matching `expected_domains'
    588          *
    589          * Only applies to v1 orders.
    590          */
    591         var forcedTokenSel: Bool?
    592         /**
    593          * Only applies to v1 orders.
    594          */
    595         var choiceIndex: Int?
    596     }
    597 }
    598 // MARK: -
    599 extension WalletModel {
    600     /// load payment details. Networking involved
    601     nonisolated func checkPayForTemplate(_ talerPayTemplateUri: String, viewHandles: Bool = false)
    602       async throws -> WalletTemplateDetails {
    603         let request = CheckPayForTemplate(talerPayTemplateUri: talerPayTemplateUri)
    604         let response = try await sendRequest(request, viewHandles: viewHandles)
    605         return response
    606     }
    607 
    608     nonisolated func preparePayForTemplate(_ talerPayTemplateUri: String, amount: Amount?, summary: String?, viewHandles: Bool = false)
    609       async throws -> PreparePayResult2 {
    610         let templateParams = TemplateParams(amount: amount, summary: summary)
    611         let request = PreparePayForTemplateRequest(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams)
    612         let response = try await sendRequest(request, viewHandles: viewHandles)
    613         return response
    614     }
    615 
    616     nonisolated func getChoicesForPayment(_ transactionId: String, viewHandles: Bool = false)
    617       async throws -> ChoicesForPayment {
    618         let request = GetChoicesForPayment(transactionId: transactionId, forcedCoinSel: nil)
    619         let response = try await sendRequest(request, viewHandles: viewHandles)
    620         return response
    621     }
    622 
    623     nonisolated func preparePayForUri(_ talerPayUri: String, viewHandles: Bool = false)
    624       async throws -> PreparePayResult2 {
    625         let request = PreparePayForUri(talerPayUri: talerPayUri)
    626         let response = try await sendRequest(request, viewHandles: viewHandles)
    627         return response
    628     }
    629 
    630     nonisolated func confirmPay(_ transactionId: String, choiceIndex: Int?, viewHandles: Bool = false)
    631       async throws -> ConfirmPayResult {
    632         let request = ConfirmPayForUri(transactionId: transactionId,
    633                                        choiceIndex: choiceIndex)
    634         let response = try await sendRequest(request, viewHandles: viewHandles)
    635         return response
    636     }
    637 }