taler-ios

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

Model+Payment.swift (25538B)


      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, Hashable {
    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 /// wallet-core sends a union discriminated on "type" - a tax-receipt output has neither
    143 /// "key_index" nor "token_family_slug", so decoding it as ContractOutput throws and takes
    144 /// the whole getChoicesForPayment response with it, leaving nothing for the user to confirm.
    145 enum ContractOutputAny: Codable, Hashable {
    146     case token(ContractOutput)
    147     case taxReceipt(ContractOutputTaxReceipt)
    148     case unknown(String)                        // a type added after this was written
    149 
    150     private enum TypeKey: String, CodingKey {
    151         case type
    152     }
    153 
    154     init(from decoder: Decoder) throws {
    155         let container = try decoder.container(keyedBy: TypeKey.self)
    156         switch try container.decode(String.self, forKey: .type) {
    157             case "token":       self = .token(try ContractOutput(from: decoder))
    158             case "tax-receipt": self = .taxReceipt(try ContractOutputTaxReceipt(from: decoder))
    159             case let other:     self = .unknown(other)
    160         }
    161     }
    162 
    163     func encode(to encoder: Encoder) throws {
    164         switch self {
    165             case .token(let output):      try output.encode(to: encoder)
    166             case .taxReceipt(let output): try output.encode(to: encoder)
    167             case .unknown(let type):
    168                 var container = encoder.container(keyedBy: TypeKey.self)
    169                 try container.encode(type, forKey: .type)
    170         }
    171     }
    172 }
    173 
    174 struct ContractChoice: Codable, Hashable {
    175     let amount: Amount                          // Total amount payable
    176     let maxFee: Amount                          // Maximum deposit fee covered by the merchant
    177     let description: String?                    //
    178     let descriptionI18n: I18nDict?              //      "      localized     "
    179     let inputs: [ContractInput]
    180     let outputs: [ContractOutputAny]
    181 
    182     enum CodingKeys: String, CodingKey {
    183         case amount, description
    184         case maxFee = "max_fee"
    185         case descriptionI18n = "description_i18n"
    186         case inputs, outputs
    187     }
    188 
    189     var descI18n: String? {
    190         if let i18nDict = self.descriptionI18n {
    191             if !i18nDict.isEmpty {
    192                 for code in Locale.preferredLanguageCodes {
    193                     if let descI18n = i18nDict[code] {
    194                         return descI18n
    195                     }
    196                 }
    197             }
    198         }
    199         if let desc = self.description {
    200             return desc
    201         }
    202         return nil
    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: [Product]?            // 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 // MARK: - Auditor
    287 struct Auditor: Codable {
    288     let name: String
    289     let auditorPub: String
    290     let url: String
    291 
    292     enum CodingKeys: String, CodingKey {
    293         case name
    294         case auditorPub = "auditor_pub"
    295         case url
    296     }
    297 }
    298 // MARK: - Exchange
    299 struct ExchangeForPay: Codable {
    300     let url: String
    301     let masterPub: String
    302 
    303     enum CodingKeys: String, CodingKey {
    304         case url
    305         case masterPub = "master_pub"
    306     }
    307 }
    308 // MARK: - Extra
    309 struct Extra: Codable {
    310     let articleName: String?
    311 
    312     enum CodingKeys: String, CodingKey {
    313         case articleName = "article_name"
    314     }
    315 }
    316 // MARK: -
    317 enum PreparePayResultType: String, Codable {
    318     case paymentPossible = "payment-possible"
    319     case alreadyConfirmed = "already-confirmed"
    320     case insufficientBalance = "insufficient-balance"
    321     case choiceSelection = "choice-selection"
    322 }
    323 
    324 struct ExchangeFeeGapEstimate: Codable {
    325     let balanceAvailable: Amount
    326     let balanceMaterial: Amount
    327     let balanceExchangeDepositable: Amount
    328     let balanceAgeAcceptable: Amount
    329     let balanceReceiverAcceptable: Amount
    330     let balanceReceiverDepositable: Amount
    331     let maxEffectiveSpendAmount: Amount
    332 }
    333 
    334 struct PerScopeDetails: Codable {
    335     let scopeInfo: ScopeInfo
    336 }
    337 
    338 /// The result from PreparePayForUri2 and preparePayForTemplate2
    339 struct PreparePayResult2: Codable {
    340     let transactionId: String
    341 }
    342 /// A request to get an exchange's payment contract terms.
    343 fileprivate struct PreparePayForUri: WalletBackendFormattedRequest {
    344     typealias Response = PreparePayResult2
    345     var operation: String { "preparePayForUriV2" }
    346     func args() -> Args { Args(talerPayUri: talerPayUri, progressToken: talerPayUri ) }
    347 
    348     var talerPayUri: String
    349     struct Args: Encodable {
    350         var talerPayUri: String
    351         var progressToken: String
    352     }
    353 }
    354 
    355 /**
    356  * Forced coin selection for deposits/payments.
    357  */
    358 struct ValueContribution: Codable {
    359     var value: Amount
    360     var contribution: Amount
    361 }
    362 
    363 struct ForcedCoinSel: Codable {
    364     var coins: [ValueContribution]
    365 }
    366 
    367 enum ChoiceSelectionDetailStatus: String, Codable {
    368     case paymentPossible = "payment-possible"
    369     case insufficientBalance = "insufficient-balance"
    370 }
    371 
    372 enum TokenAvailabilityHint: String, Codable {
    373     case walletTokensAvailableInsufficient = "wallet-tokens-available-insufficient"
    374     case merchantUnexpected = "merchant-unexpected"
    375     case merchantUntrusted = "merchant-untrusted"
    376 
    377 }
    378 
    379 struct TokenFamily: Codable, Hashable, Equatable {
    380     var causeHint: TokenAvailabilityHint?
    381     var requested: Int
    382     var available: Int
    383     var unexpected: Int
    384     var untrusted: Int
    385 }
    386 
    387 struct TokenDetails: Codable, Hashable, Equatable {
    388     var tokensRequested: Int
    389     var tokensAvailable: Int
    390     var tokensUnexpected: Int
    391     var tokensUntrusted: Int
    392     var perTokenFamily: [String: TokenFamily]
    393 }
    394 
    395 struct ChoiceSelectionDetail: Codable, Hashable, Sendable {
    396     var status: ChoiceSelectionDetailStatus
    397     var amountRaw: Amount
    398     var scopeInfo: ScopeInfo?                                   // only if wallet-core has the info
    399     var amountEffective: Amount?                                // only if possible
    400     var tokenDetails: TokenDetails?                             // only if possible
    401     var balanceDetails: PaymentInsufficientBalanceDetails?      // only if insufficient
    402 }
    403 
    404 typealias ChoiceTriple = (ChoiceSelectionDetail, ContractChoice, Int)
    405 typealias ChoicesTuple = (String?, ChoicesForPayment?)        // txID
    406 
    407 struct ChoicesForPayment: Codable {
    408     var choices: [ChoiceSelectionDetail]
    409     /**
    410      * Index of the choice in @e choices array to present to the user as default.
    411      * Won´t be set if no default selection is configured or no choice is payable,
    412      * otherwise, it will always be 0 for v0 orders.
    413      */
    414     var defaultChoiceIndex: Int?
    415     /**
    416      * Whether the choice referenced by @e automaticExecutableIndex
    417      * should be confirmed automatically without user interaction.
    418      *
    419      * If true, the wallet should call `confirmPay' immediately afterwards
    420      * If false, the user should be first prompted to select and confirm a choice.
    421      * Undefined when no choices are payable.
    422      */
    423     var automaticExecution: Bool?
    424     var automaticExecutableIndex: Int?
    425 
    426     var contractTerms: MerchantContractTerms
    427 
    428     func choiceTriple() -> ([ChoiceTriple], Bool)? {
    429         let terms = self.contractTerms
    430         if let ctChoices = terms.choices {
    431             let combined = Array(zip(choices, ctChoices, ctChoices.indices))
    432             return (combined, true)
    433         } else if let amount = terms.amount {       // V0
    434             let maxFee = terms.maxFee ?? Amount.zero(currency: amount.currencyStr)
    435             let ctChoice = ContractChoice(amount: amount,
    436                                           maxFee: maxFee,
    437                                      description: terms.summary,
    438                                  descriptionI18n: terms.summaryI18n,
    439                                           inputs: [],
    440                                          outputs: [])
    441             let combined = Array(zip(choices, [ctChoice], [0]))
    442             return (combined, false)
    443         }
    444 //        symLog.log("  ❗️Yikes, neither choices nor amount in contractTerms!\n\(stack)")
    445         return nil
    446     }
    447 }
    448 
    449 /// A request to get an exchange's payment contract terms.
    450 fileprivate struct GetChoicesForPayment: WalletBackendFormattedRequest {
    451     typealias Response = ChoicesForPayment
    452     var operation: String { "getChoicesForPayment" }
    453     func args() -> Args { Args(transactionId: transactionId, forcedCoinSel: forcedCoinSel) }
    454 
    455     var transactionId: String
    456     var forcedCoinSel: ForcedCoinSel?
    457     struct Args: Encodable {
    458         var transactionId: String
    459         var forcedCoinSel: ForcedCoinSel?
    460     }
    461 }
    462 
    463 struct TemplateParams: Codable {
    464     let amount: Amount?                     // Total amount payable
    465     let summary: String?                    // Human-readable short summary of the contract
    466 }
    467 
    468 /// A request to get an exchange's payment contract terms.
    469 fileprivate struct PreparePayForTemplateRequest: WalletBackendFormattedRequest {
    470     typealias Response = PreparePayResult2
    471     var operation: String { "preparePayForTemplateV2" }
    472     func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams,
    473                                progressToken: talerPayTemplateUri) }
    474 
    475     var talerPayTemplateUri: String
    476     var templateParams: TemplateParams
    477     struct Args: Encodable {
    478         var talerPayTemplateUri: String
    479         var templateParams: TemplateParams
    480         var progressToken: String
    481     }
    482 }
    483 // MARK: -
    484 struct TemplateContractDetails: Codable {
    485     let summary: String?                // Human-readable short summary of the contract. Editable if nil
    486     let currency: String?               // specify currency when amount is nil - unspecified if nil
    487     let amount: Amount?                 // Total amount payable. Fixed if this field exists, editable if nil
    488     let scopeInfo: ScopeInfo?
    489     let minimumAge: Int?
    490     let payDuration: Duration?
    491     let maxPickupDuration: Duration?
    492     let websiteRegex: String?
    493     let choices: [OrderChoice]?
    494     let templateType: String?
    495 
    496     enum CodingKeys: String, CodingKey {
    497         case summary, currency, amount
    498         case scopeInfo, choices
    499         case minimumAge = "minimum_age"
    500         case payDuration = "pay_duration"
    501         case maxPickupDuration = "max_pickup_duration"
    502         case websiteRegex = "website_regex"
    503         case templateType = "template_type"
    504     }
    505 }
    506 
    507 struct OrderChoice: Codable {
    508     let amount: Amount
    509     let tip: Amount?
    510     let description: String?
    511     let descriptionI18n: I18nDict?
    512     let inputs: [OrderInput]?
    513     let outputs: [OrderOutput]?     // TODO: OrderOutputTaxReceipt
    514     let maxFee: Amount?
    515 
    516     enum CodingKeys: String, CodingKey {
    517         case amount, tip, description
    518         case descriptionI18n = "description_i18n"
    519         case inputs, outputs
    520         case maxFee = "max_fee"
    521     }
    522 }
    523 
    524 struct OrderInput: Codable {    // see ContractInput
    525     let type: String                            // "token"
    526 
    527     // Token family slug as configured in the merchant backend.
    528     // Slug is unique across all configured tokens of a merchant.
    529     let tokenFamilySlug: String?
    530 
    531     // How many units of the input are required.
    532     // Defaults to 1 if not specified.
    533     // Output with count == 0 are ignored by the merchant backend.
    534     let count: Int?
    535 
    536     enum CodingKeys: String, CodingKey {
    537         case type, count
    538         case tokenFamilySlug = "token_family_slug"
    539     }
    540 }
    541 
    542 struct OrderOutput: Codable {   // TODO: ContractOutput
    543     let type: String                            // "token"
    544 
    545     // Token family slug as configured in the merchant backend.
    546     // Slug is unique across all configured tokens of a merchant.
    547     let tokenFamilySlug: String?
    548 
    549     // How many units of the output are issued by the merchant.
    550     // Defaults to 1 if not specified.
    551     // Output with count == 0 are ignored by the merchant backend.
    552     let count: Int?
    553 
    554     // When should the output token be valid. Can be specified if the
    555     // desired validity period should be in the future (like selling
    556     // a subscription for the next month). Optional. If not given,
    557     // the validity is supposed to be "now" (time of order creation).
    558     let validAt: Timestamp?
    559 
    560     enum CodingKeys: String, CodingKey {
    561         case type, count
    562         case tokenFamilySlug = "token_family_slug"
    563         case validAt = "valid_at"
    564     }
    565 }
    566 
    567 struct OrderOutputTaxReceipt: Codable {
    568     let type: String                            // "tax-receipt"
    569 }
    570 
    571 struct TemplateContractDetailsDefaults: Codable {
    572     let summary: String?                // Default 'Human-readable summary' when editable: empty if nil
    573     let currency: String?               // Default currency when unspecified: any if nil (e.g. donations)
    574     let amount: Amount?                 // Default amount when editable: unspecified if nil
    575 }
    576 
    577 struct TalerMerchantTemplateDetails: Codable {
    578     let templateContract: TemplateContractDetails
    579     let editableDefaults: TemplateContractDetailsDefaults?
    580 //    let requiredCurrency: String?
    581     enum CodingKeys: String, CodingKey {
    582         case templateContract = "template_contract"
    583         case editableDefaults = "editable_defaults"
    584 //        case requiredCurrency = "required_currency"
    585     }
    586 }
    587 
    588 /// The result from checkPayForTemplate
    589 struct WalletTemplateDetails: Codable {
    590     let templateDetails: TalerMerchantTemplateDetails
    591     let supportedCurrencies: [String]
    592 }
    593 /// A request to get an exchange's payment contract terms.
    594 fileprivate struct CheckPayForTemplate: WalletBackendFormattedRequest {
    595     typealias Response = WalletTemplateDetails
    596     var operation: String { "checkPayForTemplate" }
    597     func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, progressToken: talerPayTemplateUri) }
    598 
    599     var talerPayTemplateUri: String
    600     struct Args: Encodable {
    601         var talerPayTemplateUri: String
    602         var progressToken: String
    603     }
    604 }
    605 // MARK: -
    606 /// The result from confirmPayForUri
    607 struct ConfirmPayResult: Decodable {
    608     var type: String?                               // done || pending
    609     var contractTerms: MerchantContractTerms?       // only if type==done
    610     var transactionId: String
    611     var lastError: TalerErrorDetail?                // might, but only if type==pending
    612 }
    613 
    614 /// A request to get an exchange's payment details.
    615 fileprivate struct ConfirmPayForUri: WalletBackendFormattedRequest {
    616     typealias Response = ConfirmPayResult
    617     var operation: String { "confirmPay" }
    618     func args() -> Args { Args(transactionId: transactionId, noWait: true,
    619                                  choiceIndex: choiceIndex,
    620                                progressToken: transactionId) }
    621     var transactionId: String
    622     var choiceIndex: Int?
    623     struct Args: Encodable {
    624         var transactionId: String
    625         var noWait: Bool?
    626         var useDonau: Bool?
    627         var sessionId: String?
    628         var forcedCoinSel: ForcedCoinSel?
    629         /**
    630          * Whether token selection should be forced
    631          * e.g. use tokens with non-matching `expected_domains'
    632          *
    633          * Only applies to v1 orders.
    634          */
    635         var forcedTokenSel: Bool?
    636         /**
    637          * Only applies to v1 orders.
    638          */
    639         var choiceIndex: Int?
    640         var progressToken: String?
    641     }
    642 }
    643 // MARK: -
    644 extension WalletModel {
    645     /// load payment details. Networking involved
    646     nonisolated func checkPayForTemplate(_ talerPayTemplateUri: String, viewHandles: Bool = false)
    647       async throws -> WalletTemplateDetails {
    648         let request = CheckPayForTemplate(talerPayTemplateUri: talerPayTemplateUri)
    649         let controller = Controller.shared
    650         controller.progressOperation = request.operation
    651         controller.progressToken = talerPayTemplateUri
    652         let response = try await sendRequest(request, viewHandles: viewHandles)
    653         return response
    654     }
    655 
    656     nonisolated func preparePayForTemplate(_ talerPayTemplateUri: String, amount: Amount?, summary: String?, viewHandles: Bool = false)
    657       async throws -> PreparePayResult2 {
    658         let templateParams = TemplateParams(amount: amount, summary: summary)
    659         let request = PreparePayForTemplateRequest(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams)
    660         let controller = Controller.shared
    661         controller.progressOperation = request.operation
    662         controller.progressToken = talerPayTemplateUri
    663         let response = try await sendRequest(request, viewHandles: viewHandles)
    664         return response
    665     }
    666 
    667     nonisolated func getChoicesForPayment(_ transactionId: String, viewHandles: Bool = false)
    668       async throws -> ChoicesForPayment {
    669         let request = GetChoicesForPayment(transactionId: transactionId, forcedCoinSel: nil)
    670         let response = try await sendRequest(request, viewHandles: viewHandles)
    671         return response
    672     }
    673 
    674     nonisolated func preparePayForUri(_ talerPayUri: String, viewHandles: Bool = false)
    675       async throws -> PreparePayResult2 {
    676         let request = PreparePayForUri(talerPayUri: talerPayUri)
    677         let controller = Controller.shared
    678         controller.progressOperation = request.operation
    679         controller.progressToken = talerPayUri
    680         let response = try await sendRequest(request, viewHandles: viewHandles)
    681         return response
    682     }
    683 
    684     nonisolated func confirmPay(_ transactionId: String, choiceIndex: Int?, viewHandles: Bool = false)
    685       async throws -> ConfirmPayResult {
    686         let request = ConfirmPayForUri(transactionId: transactionId,
    687                                        choiceIndex: choiceIndex)
    688         let controller = Controller.shared
    689         controller.progressOperation = request.operation
    690         controller.progressToken = transactionId
    691         let response = try await sendRequest(request, viewHandles: viewHandles)
    692         return response
    693     }
    694 }