taler-ios

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

Model+Payment.swift (22021B)


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