taler-ios

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

Model+Payment.swift (25350B)


      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) }  // No progressToken!
    347 
    348     var talerPayUri: String
    349     struct Args: Encodable {
    350         var talerPayUri: String
    351     }
    352 }
    353 
    354 /**
    355  * Forced coin selection for deposits/payments.
    356  */
    357 struct ValueContribution: Codable {
    358     var value: Amount
    359     var contribution: Amount
    360 }
    361 
    362 struct ForcedCoinSel: Codable {
    363     var coins: [ValueContribution]
    364 }
    365 
    366 enum ChoiceSelectionDetailStatus: String, Codable {
    367     case paymentPossible = "payment-possible"
    368     case insufficientBalance = "insufficient-balance"
    369 }
    370 
    371 enum TokenAvailabilityHint: String, Codable {
    372     case walletTokensAvailableInsufficient = "wallet-tokens-available-insufficient"
    373     case merchantUnexpected = "merchant-unexpected"
    374     case merchantUntrusted = "merchant-untrusted"
    375 
    376 }
    377 
    378 struct TokenFamily: Codable, Hashable, Equatable {
    379     var causeHint: TokenAvailabilityHint?
    380     var requested: Int
    381     var available: Int
    382     var unexpected: Int
    383     var untrusted: Int
    384 }
    385 
    386 struct TokenDetails: Codable, Hashable, Equatable {
    387     var tokensRequested: Int
    388     var tokensAvailable: Int
    389     var tokensUnexpected: Int
    390     var tokensUntrusted: Int
    391     var perTokenFamily: [String: TokenFamily]
    392 }
    393 
    394 struct ChoiceSelectionDetail: Codable, Hashable, Sendable {
    395     var status: ChoiceSelectionDetailStatus
    396     var amountRaw: Amount
    397     var scopeInfo: ScopeInfo?                                   // only if wallet-core has the info
    398     var amountEffective: Amount?                                // only if possible
    399     var tokenDetails: TokenDetails?                             // only if possible
    400     var balanceDetails: PaymentInsufficientBalanceDetails?      // only if insufficient
    401 }
    402 
    403 typealias ChoiceTriple = (ChoiceSelectionDetail, ContractChoice, Int)
    404 typealias ChoicesTuple = (String?, ChoicesForPayment?)        // txID
    405 
    406 struct ChoicesForPayment: Codable {
    407     var choices: [ChoiceSelectionDetail]
    408     /**
    409      * Index of the choice in @e choices array to present to the user as default.
    410      * Won´t be set if no default selection is configured or no choice is payable,
    411      * otherwise, it will always be 0 for v0 orders.
    412      */
    413     var defaultChoiceIndex: Int?
    414     /**
    415      * Whether the choice referenced by @e automaticExecutableIndex
    416      * should be confirmed automatically without user interaction.
    417      *
    418      * If true, the wallet should call `confirmPay' immediately afterwards
    419      * If false, the user should be first prompted to select and confirm a choice.
    420      * Undefined when no choices are payable.
    421      */
    422     var automaticExecution: Bool?
    423     var automaticExecutableIndex: Int?
    424 
    425     var contractTerms: MerchantContractTerms
    426 
    427     func choiceTriple() -> ([ChoiceTriple], Bool)? {
    428         let terms = self.contractTerms
    429         if let ctChoices = terms.choices {
    430             let combined = Array(zip(choices, ctChoices, ctChoices.indices))
    431             return (combined, true)
    432         } else if let amount = terms.amount {       // V0
    433             let maxFee = terms.maxFee ?? Amount.zero(currency: amount.currencyStr)
    434             let ctChoice = ContractChoice(amount: amount,
    435                                           maxFee: maxFee,
    436                                      description: terms.summary,
    437                                  descriptionI18n: terms.summaryI18n,
    438                                           inputs: [],
    439                                          outputs: [])
    440             let combined = Array(zip(choices, [ctChoice], [0]))
    441             return (combined, false)
    442         }
    443 //        symLog.log("  ❗️Yikes, neither choices nor amount in contractTerms!\n\(stack)")
    444         return nil
    445     }
    446 }
    447 
    448 /// A request to get an exchange's payment contract terms.
    449 fileprivate struct GetChoicesForPayment: WalletBackendFormattedRequest {
    450     typealias Response = ChoicesForPayment
    451     var operation: String { "getChoicesForPayment" }
    452     func args() -> Args { Args(transactionId: transactionId, forcedCoinSel: forcedCoinSel) }
    453 
    454     var transactionId: String
    455     var forcedCoinSel: ForcedCoinSel?
    456     struct Args: Encodable {
    457         var transactionId: String
    458         var forcedCoinSel: ForcedCoinSel?
    459     }
    460 }
    461 
    462 struct TemplateParams: Codable {
    463     let amount: Amount?                     // Total amount payable
    464     let summary: String?                    // Human-readable short summary of the contract
    465 }
    466 
    467 /// A request to get an exchange's payment contract terms.
    468 fileprivate struct PreparePayForTemplateRequest: WalletBackendFormattedRequest {
    469     typealias Response = PreparePayResult2
    470     var operation: String { "preparePayForTemplateV2" }
    471     func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams,
    472                                progressToken: talerPayTemplateUri) }
    473 
    474     var talerPayTemplateUri: String
    475     var templateParams: TemplateParams
    476     struct Args: Encodable {
    477         var talerPayTemplateUri: String
    478         var templateParams: TemplateParams
    479         var progressToken: String
    480     }
    481 }
    482 // MARK: -
    483 struct TemplateContractDetails: Codable {
    484     let summary: String?                // Human-readable short summary of the contract. Editable if nil
    485     let currency: String?               // specify currency when amount is nil - unspecified if nil
    486     let amount: Amount?                 // Total amount payable. Fixed if this field exists, editable if nil
    487     let scopeInfo: ScopeInfo?
    488     let minimumAge: Int?
    489     let payDuration: Duration?
    490     let maxPickupDuration: Duration?
    491     let websiteRegex: String?
    492     let choices: [OrderChoice]?
    493     let templateType: String?
    494 
    495     enum CodingKeys: String, CodingKey {
    496         case summary, currency, amount
    497         case scopeInfo, choices
    498         case minimumAge = "minimum_age"
    499         case payDuration = "pay_duration"
    500         case maxPickupDuration = "max_pickup_duration"
    501         case websiteRegex = "website_regex"
    502         case templateType = "template_type"
    503     }
    504 }
    505 
    506 struct OrderChoice: Codable {
    507     let amount: Amount
    508     let tip: Amount?
    509     let description: String?
    510     let descriptionI18n: I18nDict?
    511     let inputs: [OrderInput]?
    512     let outputs: [OrderOutput]?     // TODO: OrderOutputTaxReceipt
    513     let maxFee: Amount?
    514 
    515     enum CodingKeys: String, CodingKey {
    516         case amount, tip, description
    517         case descriptionI18n = "description_i18n"
    518         case inputs, outputs
    519         case maxFee = "max_fee"
    520     }
    521 }
    522 
    523 struct OrderInput: Codable {    // see ContractInput
    524     let type: String                            // "token"
    525 
    526     // Token family slug as configured in the merchant backend.
    527     // Slug is unique across all configured tokens of a merchant.
    528     let tokenFamilySlug: String?
    529 
    530     // How many units of the input are required.
    531     // Defaults to 1 if not specified.
    532     // Output with count == 0 are ignored by the merchant backend.
    533     let count: Int?
    534 
    535     enum CodingKeys: String, CodingKey {
    536         case type, count
    537         case tokenFamilySlug = "token_family_slug"
    538     }
    539 }
    540 
    541 struct OrderOutput: Codable {   // TODO: ContractOutput
    542     let type: String                            // "token"
    543 
    544     // Token family slug as configured in the merchant backend.
    545     // Slug is unique across all configured tokens of a merchant.
    546     let tokenFamilySlug: String?
    547 
    548     // How many units of the output are issued by the merchant.
    549     // Defaults to 1 if not specified.
    550     // Output with count == 0 are ignored by the merchant backend.
    551     let count: Int?
    552 
    553     // When should the output token be valid. Can be specified if the
    554     // desired validity period should be in the future (like selling
    555     // a subscription for the next month). Optional. If not given,
    556     // the validity is supposed to be "now" (time of order creation).
    557     let validAt: Timestamp?
    558 
    559     enum CodingKeys: String, CodingKey {
    560         case type, count
    561         case tokenFamilySlug = "token_family_slug"
    562         case validAt = "valid_at"
    563     }
    564 }
    565 
    566 struct OrderOutputTaxReceipt: Codable {
    567     let type: String                            // "tax-receipt"
    568 }
    569 
    570 struct TemplateContractDetailsDefaults: Codable {
    571     let summary: String?                // Default 'Human-readable summary' when editable: empty if nil
    572     let currency: String?               // Default currency when unspecified: any if nil (e.g. donations)
    573     let amount: Amount?                 // Default amount when editable: unspecified if nil
    574 }
    575 
    576 struct TalerMerchantTemplateDetails: Codable {
    577     let templateContract: TemplateContractDetails
    578     let editableDefaults: TemplateContractDetailsDefaults?
    579 //    let requiredCurrency: String?
    580     enum CodingKeys: String, CodingKey {
    581         case templateContract = "template_contract"
    582         case editableDefaults = "editable_defaults"
    583 //        case requiredCurrency = "required_currency"
    584     }
    585 }
    586 
    587 /// The result from checkPayForTemplate
    588 struct WalletTemplateDetails: Codable {
    589     let templateDetails: TalerMerchantTemplateDetails
    590     let supportedCurrencies: [String]
    591 }
    592 /// A request to get an exchange's payment contract terms.
    593 fileprivate struct CheckPayForTemplate: WalletBackendFormattedRequest {
    594     typealias Response = WalletTemplateDetails
    595     var operation: String { "checkPayForTemplate" }
    596     func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, progressToken: talerPayTemplateUri) }
    597 
    598     var talerPayTemplateUri: String
    599     struct Args: Encodable {
    600         var talerPayTemplateUri: String
    601         var progressToken: String
    602     }
    603 }
    604 // MARK: -
    605 /// The result from confirmPayForUri
    606 struct ConfirmPayResult: Decodable {
    607     var type: String?                               // done || pending
    608     var contractTerms: MerchantContractTerms?       // only if type==done
    609     var transactionId: String
    610     var lastError: TalerErrorDetail?                // might, but only if type==pending
    611 }
    612 
    613 /// A request to get an exchange's payment details.
    614 fileprivate struct ConfirmPayForUri: WalletBackendFormattedRequest {
    615     typealias Response = ConfirmPayResult
    616     var operation: String { "confirmPay" }
    617     func args() -> Args { Args(transactionId: transactionId, noWait: true,
    618                                  choiceIndex: choiceIndex,
    619                                progressToken: transactionId) }
    620     var transactionId: String
    621     var choiceIndex: Int?
    622     struct Args: Encodable {
    623         var transactionId: String
    624         var noWait: Bool?
    625         var useDonau: Bool?
    626         var sessionId: String?
    627         var forcedCoinSel: ForcedCoinSel?
    628         /**
    629          * Whether token selection should be forced
    630          * e.g. use tokens with non-matching `expected_domains'
    631          *
    632          * Only applies to v1 orders.
    633          */
    634         var forcedTokenSel: Bool?
    635         /**
    636          * Only applies to v1 orders.
    637          */
    638         var choiceIndex: Int?
    639         var progressToken: String?
    640     }
    641 }
    642 // MARK: -
    643 extension WalletModel {
    644     /// load payment details. Networking involved
    645     nonisolated func checkPayForTemplate(_ talerPayTemplateUri: String, viewHandles: Bool = false)
    646       async throws -> WalletTemplateDetails {
    647         let request = CheckPayForTemplate(talerPayTemplateUri: talerPayTemplateUri)
    648         let controller = Controller.shared
    649         controller.progressOperation = request.operation
    650         controller.progressToken = talerPayTemplateUri
    651         let response = try await sendRequest(request, viewHandles: viewHandles)
    652         return response
    653     }
    654 
    655     nonisolated func preparePayForTemplate(_ talerPayTemplateUri: String, amount: Amount?, summary: String?, viewHandles: Bool = false)
    656       async throws -> PreparePayResult2 {
    657         let templateParams = TemplateParams(amount: amount, summary: summary)
    658         let request = PreparePayForTemplateRequest(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams)
    659         let controller = Controller.shared
    660         controller.progressOperation = request.operation
    661         controller.progressToken = talerPayTemplateUri
    662         let response = try await sendRequest(request, viewHandles: viewHandles)
    663         return response
    664     }
    665 
    666     nonisolated func getChoicesForPayment(_ transactionId: String, viewHandles: Bool = false)
    667       async throws -> ChoicesForPayment {
    668         let request = GetChoicesForPayment(transactionId: transactionId, forcedCoinSel: nil)
    669         let response = try await sendRequest(request, viewHandles: viewHandles)
    670         return response
    671     }
    672 
    673     nonisolated func preparePayForUri(_ talerPayUri: String, viewHandles: Bool = false)
    674       async throws -> PreparePayResult2 {
    675         let request = PreparePayForUri(talerPayUri: talerPayUri)
    676         let response = try await sendRequest(request, viewHandles: viewHandles)
    677         return response
    678     }
    679 
    680     nonisolated func confirmPay(_ transactionId: String, choiceIndex: Int?, viewHandles: Bool = false)
    681       async throws -> ConfirmPayResult {
    682         let request = ConfirmPayForUri(transactionId: transactionId,
    683                                        choiceIndex: choiceIndex)
    684         let controller = Controller.shared
    685         controller.progressOperation = request.operation
    686         controller.progressToken = transactionId
    687         let response = try await sendRequest(request, viewHandles: viewHandles)
    688         return response
    689     }
    690 }