taler-ios

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

Model+Payment.swift (27299B)


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