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 }