taler-ios

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

Model+Payment.swift (22324B)


      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 
    158 struct MerchantContractTerms: Codable {
    159     let version: Int?                   // v0 doesn't know this
    160 
    161     // ContractTermsV0
    162     let amount: Amount?                 // Total amount payable
    163     let maxFee: Amount?                 // Maximum deposit fee covered by the merchant
    164 
    165     // ContractTermsCommon
    166     let summary: String                 // Human-readable short summary of the contract
    167     let summaryI18n: I18nDict?          //      "      localized     "
    168     let orderID: String                 // uniquely identify the purchase within one merchant instance
    169     let publicReorderURL: String?       // URL meant to share the shopping cart
    170     let fulfillmentURL: String?         // Fulfillment URL to view the product or delivery status
    171     let fulfillmentMessage: String?     // Plain text fulfillment message in the merchant's default language
    172     let fulfillmentMessageI18n: String? // Plain text fulfillment message in the merchant's default language
    173     let products: [Product]?            // Products that are sold in this contract
    174     let timestamp: Timestamp            // Time when the contract was generated by the merchant
    175     let refundDeadline: Timestamp?      // Deadline for refunds
    176     let payDeadline: Timestamp          // Deadline to pay for the contract
    177     let wireTransferDeadline: Timestamp?// Deadline for the wire transfer
    178     let merchantPub: String             // Public key of the merchant
    179     let merchantBaseURL: String         // Base URL of the merchant's backend
    180     let merchant: Merchant
    181     let hWire: String                   // Hash of the merchant's wire details
    182     let wireMethod: String              // merchant wants to use
    183     let exchanges: [ExchangeForPay]
    184     let deliveryLocation: Location?     // Delivery location for (all!) products
    185     let deliveryDate: Timestamp?        // indicating when the order should be delivered
    186     let nonce: String                   // used to ensure freshness
    187     let autoRefund: Duration?
    188     let extra: Extra?                   // Extra data, interpreted by the merchant only
    189     let minimumAge: Int?
    190 
    191     let defaultMoneyPot: Int?
    192 
    193 // deprecated   let wireFeeAmortization: Int?       // Share of the wire fee that must be settled with one payment
    194 //    let maxWireFee: Amount?             // Maximum wire fee that the merchant agrees to pay for
    195 //    let auditors: [Auditor]?
    196 
    197     // ContractTermsV1
    198     let choices: [ContractChoice]?
    199     // Map of storing metadata and issue keys of
    200     // token families referenced in this contract.
    201     // @since protocol **vSUBSCRIBE**
    202     let tokenFamilies: [String: ContractTokenFamily]?   // token_family_slug: String
    203 
    204     enum CodingKeys: String, CodingKey {
    205         case version
    206         case amount
    207         case maxFee = "max_fee"
    208 
    209         case summary
    210         case summaryI18n = "summary_i18n"
    211         case orderID = "order_id"
    212         case publicReorderURL = "public_reorder_url"
    213         case fulfillmentURL = "fulfillment_url"
    214         case fulfillmentMessage = "fulfillment_message"
    215         case fulfillmentMessageI18n = "fulfillment_message_i18n"
    216         case products
    217         case timestamp
    218         case refundDeadline = "refund_deadline"
    219         case payDeadline = "pay_deadline"
    220         case wireTransferDeadline = "wire_transfer_deadline"
    221         case merchantPub = "merchant_pub"
    222         case merchantBaseURL = "merchant_base_url"
    223         case merchant
    224         case hWire = "h_wire"
    225         case wireMethod = "wire_method"
    226         case exchanges
    227         case deliveryLocation = "delivery_location"
    228         case deliveryDate = "delivery_date"
    229         case nonce
    230         case autoRefund = "auto_refund"
    231         case extra
    232         case minimumAge = "minimum_age"
    233         case defaultMoneyPot = "default_money_pot"
    234 
    235 //        case wireFeeAmortization = "wire_fee_amortization"
    236 //        case maxWireFee = "max_wire_fee"
    237 //        case auditors
    238         case choices
    239         case tokenFamilies = "token_families"
    240     }
    241 }
    242 // MARK: - Auditor
    243 struct Auditor: Codable {
    244     let name: String
    245     let auditorPub: String
    246     let url: String
    247 
    248     enum CodingKeys: String, CodingKey {
    249         case name
    250         case auditorPub = "auditor_pub"
    251         case url
    252     }
    253 }
    254 // MARK: - Exchange
    255 struct ExchangeForPay: Codable {
    256     let url: String
    257     let masterPub: String
    258 
    259     enum CodingKeys: String, CodingKey {
    260         case url
    261         case masterPub = "master_pub"
    262     }
    263 }
    264 // MARK: - Extra
    265 struct Extra: Codable {
    266     let articleName: String?
    267 
    268     enum CodingKeys: String, CodingKey {
    269         case articleName = "article_name"
    270     }
    271 }
    272 // MARK: -
    273 enum PreparePayResultType: String, Codable {
    274     case paymentPossible = "payment-possible"
    275     case alreadyConfirmed = "already-confirmed"
    276     case insufficientBalance = "insufficient-balance"
    277     case choiceSelection = "choice-selection"
    278 }
    279 
    280 struct PayMerchantInsufficientBalanceDetails: Codable {
    281     let amountRequested: Amount
    282     let balanceAvailable: Amount
    283     let balanceMaterial: Amount
    284     let balanceAgeAcceptable: Amount
    285     let balanceReceiverAcceptable: Amount
    286     let balanceReceiverDepositable: Amount
    287     let perExchange: [String:ExchangeFeeGapEstimate]
    288     let causeHint: InsufficientBalanceHint
    289 }
    290 
    291 struct ExchangeFeeGapEstimate: Codable {
    292     let balanceAvailable: Amount
    293     let balanceMaterial: Amount
    294     let balanceExchangeDepositable: Amount
    295     let balanceAgeAcceptable: Amount
    296     let balanceReceiverAcceptable: Amount
    297     let balanceReceiverDepositable: Amount
    298     let maxEffectiveSpendAmount: Amount
    299 }
    300 
    301 struct PerScopeDetails: Codable {
    302     let scopeInfo: ScopeInfo
    303 }
    304 
    305 /// The result from PreparePayForUri2 and preparePayForTemplate2
    306 struct PreparePayResult2: Codable {
    307     let transactionId: String
    308 }
    309 /// A request to get an exchange's payment contract terms.
    310 fileprivate struct PreparePayForUri: WalletBackendFormattedRequest {
    311     typealias Response = PreparePayResult2
    312     func operation() -> String { "preparePayForUriV2" }
    313     func args() -> Args { Args(talerPayUri: talerPayUri) }
    314 
    315     var talerPayUri: String
    316     struct Args: Encodable {
    317         var talerPayUri: String
    318     }
    319 }
    320 
    321 /**
    322  * Forced coin selection for deposits/payments.
    323  */
    324 struct ValueContribution: Codable {
    325     var value: Amount
    326     var contribution: Amount
    327 }
    328 struct ForcedCoinSel: Codable {
    329     var coins: [ValueContribution]
    330 }
    331 
    332 enum ChoiceSelectionDetailStatus: String, Codable {
    333     case paymentPossible = "payment-possible"
    334     case insufficientBalance = "insufficient-balance"
    335 }
    336 
    337 enum TokenAvailabilityHint: String, Codable {
    338     case walletTokensAvailableInsufficient = "wallet-tokens-available-insufficient"
    339     case merchantUnexpected = "merchant-unexpected"
    340     case merchantUntrusted = "merchant-untrusted"
    341 
    342 }
    343 struct TokenFamily: Codable, Hashable, Equatable {
    344     var causeHint: TokenAvailabilityHint?
    345     var requested: Int
    346     var available: Int
    347     var unexpected: Int
    348     var untrusted: Int
    349 }
    350 
    351 struct TokenDetails: Codable, Hashable, Equatable {
    352     var tokensRequested: Int
    353     var tokensAvailable: Int
    354     var tokensUnexpected: Int
    355     var tokensUntrusted: Int
    356     var perTokenFamily: [String: TokenFamily]
    357 }
    358 
    359 struct ChoiceSelectionDetail: Codable, Hashable, Sendable {
    360     var status: ChoiceSelectionDetailStatus
    361     var amountRaw: Amount
    362     var scopeInfo: ScopeInfo?                                   // only if wallet-core has the info
    363     var amountEffective: Amount?                                // only if possible
    364     var tokenDetails: TokenDetails?                             // only if possible
    365     var balanceDetails: PaymentInsufficientBalanceDetails?      // only if insufficient
    366 }
    367 
    368 struct GetChoicesForPaymentResult: Codable {
    369     var choices: [ChoiceSelectionDetail]
    370     /**
    371      * Index of the choice in @e choices array to present to the user as default.
    372      * Won´t be set if no default selection is configured or no choice is payable,
    373      * otherwise, it will always be 0 for v0 orders.
    374      */
    375     var defaultChoiceIndex: Int?
    376     /**
    377      * Whether the choice referenced by @e automaticExecutableIndex
    378      * should be confirmed automatically without user interaction.
    379      *
    380      * If true, the wallet should call `confirmPay' immediately afterwards
    381      * If false, the user should be first prompted to select and confirm a choice.
    382      * Undefined when no choices are payable.
    383      */
    384     var automaticExecution: Bool?
    385     var automaticExecutableIndex: Int?
    386 
    387     var contractTerms: MerchantContractTerms
    388 }
    389 
    390 /// A request to get an exchange's payment contract terms.
    391 fileprivate struct GetChoicesForPayment: WalletBackendFormattedRequest {
    392     typealias Response = GetChoicesForPaymentResult
    393     func operation() -> String { "getChoicesForPayment" }
    394     func args() -> Args { Args(transactionId: transactionId, forcedCoinSel: forcedCoinSel) }
    395 
    396     var transactionId: String
    397     var forcedCoinSel: ForcedCoinSel?
    398     struct Args: Encodable {
    399         var transactionId: String
    400         var forcedCoinSel: ForcedCoinSel?
    401     }
    402 }
    403 
    404 struct TemplateParams: Codable {
    405     let amount: Amount?                     // Total amount payable
    406     let summary: String?                    // Human-readable short summary of the contract
    407 }
    408 /// A request to get an exchange's payment contract terms.
    409 fileprivate struct PreparePayForTemplateRequest: WalletBackendFormattedRequest {
    410     typealias Response = PreparePayResult2
    411     func operation() -> String { "preparePayForTemplateV2" }
    412     func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams) }
    413 
    414     var talerPayTemplateUri: String
    415     var templateParams: TemplateParams
    416     struct Args: Encodable {
    417         var talerPayTemplateUri: String
    418         var templateParams: TemplateParams
    419     }
    420 }
    421 // MARK: -
    422 struct TemplateContractDetails: Codable {
    423     let summary: String?                // Human-readable short summary of the contract. Editable if nil
    424     let currency: String?               // specify currency when amount is nil - unspecified if nil
    425     let amount: Amount?                 // Total amount payable. Fixed if this field exists, editable if nil
    426     let scopeInfo: ScopeInfo?
    427     let minimumAge: Int?
    428     let payDuration: Duration?
    429     let maxPickupDuration: Duration?
    430     let websiteRegex: String?
    431     let choices: [OrderChoice]?
    432     let templateType: String?
    433 
    434     enum CodingKeys: String, CodingKey {
    435         case summary, currency, amount
    436         case scopeInfo, choices
    437         case minimumAge = "minimum_age"
    438         case payDuration = "pay_duration"
    439         case maxPickupDuration = "max_pickup_duration"
    440         case websiteRegex = "website_regex"
    441         case templateType = "template_type"
    442     }
    443 }
    444 
    445 struct OrderChoice: Codable {
    446     let amount: Amount
    447     let tip: Amount?
    448     let description: String?
    449     let descriptionI18n: I18nDict?
    450     let inputs: [OrderInput]?
    451     let outputs: [OrderOutput]?     // TODO: OrderOutputTaxReceipt
    452     let maxFee: Amount?
    453 
    454     enum CodingKeys: String, CodingKey {
    455         case amount, tip, description
    456         case descriptionI18n = "description_i18n"
    457         case inputs, outputs
    458         case maxFee = "max_fee"
    459     }
    460 }
    461 
    462 struct OrderInput: Codable {    // see ContractInput
    463     let type: String                            // "token"
    464 
    465     // Token family slug as configured in the merchant backend.
    466     // Slug is unique across all configured tokens of a merchant.
    467     let tokenFamilySlug: String?
    468 
    469     // How many units of the input are required.
    470     // Defaults to 1 if not specified.
    471     // Output with count == 0 are ignored by the merchant backend.
    472     let count: Int?
    473 
    474     enum CodingKeys: String, CodingKey {
    475         case type, count
    476         case tokenFamilySlug = "token_family_slug"
    477     }
    478 }
    479 
    480 struct OrderOutput: Codable {   // TODO: ContractOutput
    481     let type: String                            // "token"
    482 
    483     // Token family slug as configured in the merchant backend.
    484     // Slug is unique across all configured tokens of a merchant.
    485     let tokenFamilySlug: String?
    486 
    487     // How many units of the output are issued by the merchant.
    488     // Defaults to 1 if not specified.
    489     // Output with count == 0 are ignored by the merchant backend.
    490     let count: Int?
    491 
    492     // When should the output token be valid. Can be specified if the
    493     // desired validity period should be in the future (like selling
    494     // a subscription for the next month). Optional. If not given,
    495     // the validity is supposed to be "now" (time of order creation).
    496     let validAt: Timestamp?
    497 
    498     enum CodingKeys: String, CodingKey {
    499         case type, count
    500         case tokenFamilySlug = "token_family_slug"
    501         case validAt = "valid_at"
    502     }
    503 }
    504 struct OrderOutputTaxReceipt: Codable {
    505     let type: String                            // "tax-receipt"
    506 }
    507 
    508 struct TemplateContractDetailsDefaults: Codable {
    509     let summary: String?                // Default 'Human-readable summary' when editable: empty if nil
    510     let currency: String?               // Default currency when unspecified: any if nil (e.g. donations)
    511     let amount: Amount?                 // Default amount when editable: unspecified if nil
    512 }
    513 struct TalerMerchantTemplateDetails: Codable {
    514     let templateContract: TemplateContractDetails
    515     let editableDefaults: TemplateContractDetailsDefaults?
    516 //    let requiredCurrency: String?
    517     enum CodingKeys: String, CodingKey {
    518         case templateContract = "template_contract"
    519         case editableDefaults = "editable_defaults"
    520 //        case requiredCurrency = "required_currency"
    521     }
    522 }
    523 
    524 /// The result from checkPayForTemplate
    525 struct WalletTemplateDetails: Codable {
    526     let templateDetails: TalerMerchantTemplateDetails
    527     let supportedCurrencies: [String]
    528 }
    529 /// A request to get an exchange's payment contract terms.
    530 fileprivate struct CheckPayForTemplate: WalletBackendFormattedRequest {
    531     typealias Response = WalletTemplateDetails
    532     func operation() -> String { "checkPayForTemplate" }
    533     func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri) }
    534 
    535     var talerPayTemplateUri: String
    536     struct Args: Encodable {
    537         var talerPayTemplateUri: String
    538     }
    539 }
    540 // MARK: -
    541 /// The result from confirmPayForUri
    542 struct ConfirmPayResult: Decodable {
    543     var type: String                                // done || pending
    544     var contractTerms: MerchantContractTerms?       // only if type==done
    545     var transactionId: String
    546     var lastError: TalerErrorDetail?                // might, but only if type==pending
    547 }
    548 /// A request to get an exchange's payment details.
    549 fileprivate struct ConfirmPayForUri: WalletBackendFormattedRequest {
    550     typealias Response = ConfirmPayResult
    551     func operation() -> String { "confirmPay" }
    552     func args() -> Args { Args(transactionId: transactionId, choiceIndex: choiceIndex) }
    553 
    554     var transactionId: String
    555     var choiceIndex: Int?
    556     struct Args: Encodable {
    557         var transactionId: String
    558         var useDonau: Bool?
    559         var sessionId: String?
    560         var forcedCoinSel: ForcedCoinSel?
    561         /**
    562          * Whether token selection should be forced
    563          * e.g. use tokens with non-matching `expected_domains'
    564          *
    565          * Only applies to v1 orders.
    566          */
    567         var forcedTokenSel: Bool?
    568         /**
    569          * Only applies to v1 orders.
    570          */
    571         var choiceIndex: Int?
    572     }
    573 }
    574 // MARK: -
    575 extension WalletModel {
    576     /// load payment details. Networking involved
    577     nonisolated func checkPayForTemplate(_ talerPayTemplateUri: String, viewHandles: Bool = false)
    578       async throws -> WalletTemplateDetails {
    579         let request = CheckPayForTemplate(talerPayTemplateUri: talerPayTemplateUri)
    580         let response = try await sendRequest(request, viewHandles: viewHandles)
    581         return response
    582     }
    583 
    584     nonisolated func preparePayForTemplate(_ talerPayTemplateUri: String, amount: Amount?, summary: String?, viewHandles: Bool = false)
    585       async throws -> PreparePayResult2 {
    586         let templateParams = TemplateParams(amount: amount, summary: summary)
    587         let request = PreparePayForTemplateRequest(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams)
    588         let response = try await sendRequest(request, viewHandles: viewHandles)
    589         return response
    590     }
    591 
    592     nonisolated func getChoicesForPayment(_ transactionId: String, viewHandles: Bool = false)
    593       async throws -> GetChoicesForPaymentResult {
    594         let request = GetChoicesForPayment(transactionId: transactionId, forcedCoinSel: nil)
    595         let response = try await sendRequest(request, viewHandles: viewHandles)
    596         return response
    597     }
    598 
    599     nonisolated func preparePayForUri(_ talerPayUri: String, viewHandles: Bool = false)
    600       async throws -> PreparePayResult2 {
    601         let request = PreparePayForUri(talerPayUri: talerPayUri)
    602         let response = try await sendRequest(request, viewHandles: viewHandles)
    603         return response
    604     }
    605 
    606     nonisolated func confirmPay(_ transactionId: String, choiceIndex: Int?, viewHandles: Bool = false)
    607       async throws -> ConfirmPayResult {
    608         let request = ConfirmPayForUri(transactionId: transactionId,
    609                                        choiceIndex: choiceIndex)
    610         let response = try await sendRequest(request, viewHandles: viewHandles)
    611         return response
    612     }
    613 }