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 }