Model+Payment.swift (23173B)
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 { 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 struct ContractChoice: Codable, Hashable { 143 let amount: Amount // Total amount payable 144 let maxFee: Amount // Maximum deposit fee covered by the merchant 145 let description: String? // 146 let descriptionI18n: I18nDict? // " localized " 147 let inputs: [ContractInput] 148 let outputs: [ContractOutput] 149 150 enum CodingKeys: String, CodingKey { 151 case amount, description 152 case maxFee = "max_fee" 153 case descriptionI18n = "description_i18n" 154 case inputs, outputs 155 } 156 157 var descI18n: String? { 158 if let i18nDict = self.descriptionI18n { 159 if !i18nDict.isEmpty { 160 for code in Locale.preferredLanguageCodes { 161 if let descI18n = i18nDict[code] { 162 return descI18n 163 } 164 } 165 } 166 } 167 if let desc = self.description { 168 return desc 169 } 170 return nil 171 } 172 } 173 174 struct MerchantContractTerms: Codable { 175 let version: Int? // v0 doesn't know this 176 177 // ContractTermsV0 178 let amount: Amount? // Total amount payable 179 let maxFee: Amount? // Maximum deposit fee covered by the merchant 180 181 // ContractTermsCommon 182 let summary: String // Human-readable short summary of the contract 183 let summaryI18n: I18nDict? // " localized " 184 let orderID: String // uniquely identify the purchase within one merchant instance 185 let publicReorderURL: String? // URL meant to share the shopping cart 186 let fulfillmentURL: String? // Fulfillment URL to view the product or delivery status 187 let fulfillmentMessage: String? // Plain text fulfillment message in the merchant's default language 188 let fulfillmentMessageI18n: String? // Plain text fulfillment message in the merchant's default language 189 let products: [Product]? // Products that are sold in this contract 190 let timestamp: Timestamp // Time when the contract was generated by the merchant 191 let refundDeadline: Timestamp? // Deadline for refunds 192 let payDeadline: Timestamp // Deadline to pay for the contract 193 let wireTransferDeadline: Timestamp?// Deadline for the wire transfer 194 let merchantPub: String // Public key of the merchant 195 let merchantBaseURL: String // Base URL of the merchant's backend 196 let merchant: MerchantInfo 197 let hWire: String // Hash of the merchant's wire details 198 let wireMethod: String // merchant wants to use 199 let exchanges: [ExchangeForPay] 200 let deliveryLocation: Location? // Delivery location for (all!) products 201 let deliveryDate: Timestamp? // indicating when the order should be delivered 202 let nonce: String // used to ensure freshness 203 let autoRefund: Duration? 204 let extra: Extra? // Extra data, interpreted by the merchant only 205 let minimumAge: Int? 206 207 let defaultMoneyPot: Int? 208 209 // deprecated let auditors: [Auditor]? 210 211 // ContractTermsV1 212 let choices: [ContractChoice]? 213 // Map of storing metadata and issue keys of 214 // token families referenced in this contract. 215 // @since protocol **vSUBSCRIBE** 216 let tokenFamilies: [String: ContractTokenFamily]? // token_family_slug: String 217 218 enum CodingKeys: String, CodingKey { 219 case version 220 case amount 221 case maxFee = "max_fee" 222 223 case summary 224 case summaryI18n = "summary_i18n" 225 case orderID = "order_id" 226 case publicReorderURL = "public_reorder_url" 227 case fulfillmentURL = "fulfillment_url" 228 case fulfillmentMessage = "fulfillment_message" 229 case fulfillmentMessageI18n = "fulfillment_message_i18n" 230 case products 231 case timestamp 232 case refundDeadline = "refund_deadline" 233 case payDeadline = "pay_deadline" 234 case wireTransferDeadline = "wire_transfer_deadline" 235 case merchantPub = "merchant_pub" 236 case merchantBaseURL = "merchant_base_url" 237 case merchant 238 case hWire = "h_wire" 239 case wireMethod = "wire_method" 240 case exchanges 241 case deliveryLocation = "delivery_location" 242 case deliveryDate = "delivery_date" 243 case nonce 244 case autoRefund = "auto_refund" 245 case extra 246 case minimumAge = "minimum_age" 247 case defaultMoneyPot = "default_money_pot" 248 249 // case auditors 250 case choices 251 case tokenFamilies = "token_families" 252 } 253 } 254 // MARK: - Auditor 255 struct Auditor: Codable { 256 let name: String 257 let auditorPub: String 258 let url: String 259 260 enum CodingKeys: String, CodingKey { 261 case name 262 case auditorPub = "auditor_pub" 263 case url 264 } 265 } 266 // MARK: - Exchange 267 struct ExchangeForPay: Codable { 268 let url: String 269 let masterPub: String 270 271 enum CodingKeys: String, CodingKey { 272 case url 273 case masterPub = "master_pub" 274 } 275 } 276 // MARK: - Extra 277 struct Extra: Codable { 278 let articleName: String? 279 280 enum CodingKeys: String, CodingKey { 281 case articleName = "article_name" 282 } 283 } 284 // MARK: - 285 enum PreparePayResultType: String, Codable { 286 case paymentPossible = "payment-possible" 287 case alreadyConfirmed = "already-confirmed" 288 case insufficientBalance = "insufficient-balance" 289 case choiceSelection = "choice-selection" 290 } 291 292 struct ExchangeFeeGapEstimate: Codable { 293 let balanceAvailable: Amount 294 let balanceMaterial: Amount 295 let balanceExchangeDepositable: Amount 296 let balanceAgeAcceptable: Amount 297 let balanceReceiverAcceptable: Amount 298 let balanceReceiverDepositable: Amount 299 let maxEffectiveSpendAmount: Amount 300 } 301 302 struct PerScopeDetails: Codable { 303 let scopeInfo: ScopeInfo 304 } 305 306 /// The result from PreparePayForUri2 and preparePayForTemplate2 307 struct PreparePayResult2: Codable { 308 let transactionId: String 309 } 310 /// A request to get an exchange's payment contract terms. 311 fileprivate struct PreparePayForUri: WalletBackendFormattedRequest { 312 typealias Response = PreparePayResult2 313 func operation() -> String { "preparePayForUriV2" } 314 func args() -> Args { Args(talerPayUri: talerPayUri) } 315 316 var talerPayUri: String 317 struct Args: Encodable { 318 var talerPayUri: String 319 } 320 } 321 322 /** 323 * Forced coin selection for deposits/payments. 324 */ 325 struct ValueContribution: Codable { 326 var value: Amount 327 var contribution: Amount 328 } 329 struct ForcedCoinSel: Codable { 330 var coins: [ValueContribution] 331 } 332 333 enum ChoiceSelectionDetailStatus: String, Codable { 334 case paymentPossible = "payment-possible" 335 case insufficientBalance = "insufficient-balance" 336 } 337 338 enum TokenAvailabilityHint: String, Codable { 339 case walletTokensAvailableInsufficient = "wallet-tokens-available-insufficient" 340 case merchantUnexpected = "merchant-unexpected" 341 case merchantUntrusted = "merchant-untrusted" 342 343 } 344 struct TokenFamily: Codable, Hashable, Equatable { 345 var causeHint: TokenAvailabilityHint? 346 var requested: Int 347 var available: Int 348 var unexpected: Int 349 var untrusted: Int 350 } 351 352 struct TokenDetails: Codable, Hashable, Equatable { 353 var tokensRequested: Int 354 var tokensAvailable: Int 355 var tokensUnexpected: Int 356 var tokensUntrusted: Int 357 var perTokenFamily: [String: TokenFamily] 358 } 359 360 struct ChoiceSelectionDetail: Codable, Hashable, Sendable { 361 var status: ChoiceSelectionDetailStatus 362 var amountRaw: Amount 363 var scopeInfo: ScopeInfo? // only if wallet-core has the info 364 var amountEffective: Amount? // only if possible 365 var tokenDetails: TokenDetails? // only if possible 366 var balanceDetails: PaymentInsufficientBalanceDetails? // only if insufficient 367 } 368 369 typealias ChoiceTriple = (ChoiceSelectionDetail, ContractChoice, Int) 370 371 struct ChoicesForPayment: Codable { 372 var choices: [ChoiceSelectionDetail] 373 /** 374 * Index of the choice in @e choices array to present to the user as default. 375 * Won´t be set if no default selection is configured or no choice is payable, 376 * otherwise, it will always be 0 for v0 orders. 377 */ 378 var defaultChoiceIndex: Int? 379 /** 380 * Whether the choice referenced by @e automaticExecutableIndex 381 * should be confirmed automatically without user interaction. 382 * 383 * If true, the wallet should call `confirmPay' immediately afterwards 384 * If false, the user should be first prompted to select and confirm a choice. 385 * Undefined when no choices are payable. 386 */ 387 var automaticExecution: Bool? 388 var automaticExecutableIndex: Int? 389 390 var contractTerms: MerchantContractTerms 391 392 func choiceTriple() -> ([ChoiceTriple], Bool)? { 393 let terms = self.contractTerms 394 if let ctChoices = terms.choices { 395 let combined = Array(zip(choices, ctChoices, ctChoices.indices)) 396 return (combined, true) 397 } else if let amount = terms.amount { // V0 398 let maxFee = terms.maxFee ?? Amount.zero(currency: amount.currencyStr) 399 let ctChoice = ContractChoice(amount: amount, 400 maxFee: maxFee, 401 description: terms.summary, 402 descriptionI18n: terms.summaryI18n, 403 inputs: [], 404 outputs: []) 405 let combined = Array(zip(choices, [ctChoice], [0])) 406 return (combined, false) 407 } 408 // symLog.log(" ❗️Yikes, neither choices nor amount in contractTerms!\n\(stack)") 409 return nil 410 } 411 } 412 413 /// A request to get an exchange's payment contract terms. 414 fileprivate struct GetChoicesForPayment: WalletBackendFormattedRequest { 415 typealias Response = ChoicesForPayment 416 func operation() -> String { "getChoicesForPayment" } 417 func args() -> Args { Args(transactionId: transactionId, forcedCoinSel: forcedCoinSel) } 418 419 var transactionId: String 420 var forcedCoinSel: ForcedCoinSel? 421 struct Args: Encodable { 422 var transactionId: String 423 var forcedCoinSel: ForcedCoinSel? 424 } 425 } 426 427 struct TemplateParams: Codable { 428 let amount: Amount? // Total amount payable 429 let summary: String? // Human-readable short summary of the contract 430 } 431 /// A request to get an exchange's payment contract terms. 432 fileprivate struct PreparePayForTemplateRequest: WalletBackendFormattedRequest { 433 typealias Response = PreparePayResult2 434 func operation() -> String { "preparePayForTemplateV2" } 435 func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams) } 436 437 var talerPayTemplateUri: String 438 var templateParams: TemplateParams 439 struct Args: Encodable { 440 var talerPayTemplateUri: String 441 var templateParams: TemplateParams 442 } 443 } 444 // MARK: - 445 struct TemplateContractDetails: Codable { 446 let summary: String? // Human-readable short summary of the contract. Editable if nil 447 let currency: String? // specify currency when amount is nil - unspecified if nil 448 let amount: Amount? // Total amount payable. Fixed if this field exists, editable if nil 449 let scopeInfo: ScopeInfo? 450 let minimumAge: Int? 451 let payDuration: Duration? 452 let maxPickupDuration: Duration? 453 let websiteRegex: String? 454 let choices: [OrderChoice]? 455 let templateType: String? 456 457 enum CodingKeys: String, CodingKey { 458 case summary, currency, amount 459 case scopeInfo, choices 460 case minimumAge = "minimum_age" 461 case payDuration = "pay_duration" 462 case maxPickupDuration = "max_pickup_duration" 463 case websiteRegex = "website_regex" 464 case templateType = "template_type" 465 } 466 } 467 468 struct OrderChoice: Codable { 469 let amount: Amount 470 let tip: Amount? 471 let description: String? 472 let descriptionI18n: I18nDict? 473 let inputs: [OrderInput]? 474 let outputs: [OrderOutput]? // TODO: OrderOutputTaxReceipt 475 let maxFee: Amount? 476 477 enum CodingKeys: String, CodingKey { 478 case amount, tip, description 479 case descriptionI18n = "description_i18n" 480 case inputs, outputs 481 case maxFee = "max_fee" 482 } 483 } 484 485 struct OrderInput: Codable { // see ContractInput 486 let type: String // "token" 487 488 // Token family slug as configured in the merchant backend. 489 // Slug is unique across all configured tokens of a merchant. 490 let tokenFamilySlug: String? 491 492 // How many units of the input are required. 493 // Defaults to 1 if not specified. 494 // Output with count == 0 are ignored by the merchant backend. 495 let count: Int? 496 497 enum CodingKeys: String, CodingKey { 498 case type, count 499 case tokenFamilySlug = "token_family_slug" 500 } 501 } 502 503 struct OrderOutput: Codable { // TODO: ContractOutput 504 let type: String // "token" 505 506 // Token family slug as configured in the merchant backend. 507 // Slug is unique across all configured tokens of a merchant. 508 let tokenFamilySlug: String? 509 510 // How many units of the output are issued by the merchant. 511 // Defaults to 1 if not specified. 512 // Output with count == 0 are ignored by the merchant backend. 513 let count: Int? 514 515 // When should the output token be valid. Can be specified if the 516 // desired validity period should be in the future (like selling 517 // a subscription for the next month). Optional. If not given, 518 // the validity is supposed to be "now" (time of order creation). 519 let validAt: Timestamp? 520 521 enum CodingKeys: String, CodingKey { 522 case type, count 523 case tokenFamilySlug = "token_family_slug" 524 case validAt = "valid_at" 525 } 526 } 527 struct OrderOutputTaxReceipt: Codable { 528 let type: String // "tax-receipt" 529 } 530 531 struct TemplateContractDetailsDefaults: Codable { 532 let summary: String? // Default 'Human-readable summary' when editable: empty if nil 533 let currency: String? // Default currency when unspecified: any if nil (e.g. donations) 534 let amount: Amount? // Default amount when editable: unspecified if nil 535 } 536 struct TalerMerchantTemplateDetails: Codable { 537 let templateContract: TemplateContractDetails 538 let editableDefaults: TemplateContractDetailsDefaults? 539 // let requiredCurrency: String? 540 enum CodingKeys: String, CodingKey { 541 case templateContract = "template_contract" 542 case editableDefaults = "editable_defaults" 543 // case requiredCurrency = "required_currency" 544 } 545 } 546 547 /// The result from checkPayForTemplate 548 struct WalletTemplateDetails: Codable { 549 let templateDetails: TalerMerchantTemplateDetails 550 let supportedCurrencies: [String] 551 } 552 /// A request to get an exchange's payment contract terms. 553 fileprivate struct CheckPayForTemplate: WalletBackendFormattedRequest { 554 typealias Response = WalletTemplateDetails 555 func operation() -> String { "checkPayForTemplate" } 556 func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri) } 557 558 var talerPayTemplateUri: String 559 struct Args: Encodable { 560 var talerPayTemplateUri: String 561 } 562 } 563 // MARK: - 564 /// The result from confirmPayForUri 565 struct ConfirmPayResult: Decodable { 566 var type: String // done || pending 567 var contractTerms: MerchantContractTerms? // only if type==done 568 var transactionId: String 569 var lastError: TalerErrorDetail? // might, but only if type==pending 570 } 571 /// A request to get an exchange's payment details. 572 fileprivate struct ConfirmPayForUri: WalletBackendFormattedRequest { 573 typealias Response = ConfirmPayResult 574 func operation() -> String { "confirmPay" } 575 func args() -> Args { Args(transactionId: transactionId, noWait: true, 576 choiceIndex: choiceIndex) } 577 var transactionId: String 578 var choiceIndex: Int? 579 struct Args: Encodable { 580 var transactionId: String 581 var noWait: Bool? 582 var useDonau: Bool? 583 var sessionId: String? 584 var forcedCoinSel: ForcedCoinSel? 585 /** 586 * Whether token selection should be forced 587 * e.g. use tokens with non-matching `expected_domains' 588 * 589 * Only applies to v1 orders. 590 */ 591 var forcedTokenSel: Bool? 592 /** 593 * Only applies to v1 orders. 594 */ 595 var choiceIndex: Int? 596 } 597 } 598 // MARK: - 599 extension WalletModel { 600 /// load payment details. Networking involved 601 nonisolated func checkPayForTemplate(_ talerPayTemplateUri: String, viewHandles: Bool = false) 602 async throws -> WalletTemplateDetails { 603 let request = CheckPayForTemplate(talerPayTemplateUri: talerPayTemplateUri) 604 let response = try await sendRequest(request, viewHandles: viewHandles) 605 return response 606 } 607 608 nonisolated func preparePayForTemplate(_ talerPayTemplateUri: String, amount: Amount?, summary: String?, viewHandles: Bool = false) 609 async throws -> PreparePayResult2 { 610 let templateParams = TemplateParams(amount: amount, summary: summary) 611 let request = PreparePayForTemplateRequest(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams) 612 let response = try await sendRequest(request, viewHandles: viewHandles) 613 return response 614 } 615 616 nonisolated func getChoicesForPayment(_ transactionId: String, viewHandles: Bool = false) 617 async throws -> ChoicesForPayment { 618 let request = GetChoicesForPayment(transactionId: transactionId, forcedCoinSel: nil) 619 let response = try await sendRequest(request, viewHandles: viewHandles) 620 return response 621 } 622 623 nonisolated func preparePayForUri(_ talerPayUri: String, viewHandles: Bool = false) 624 async throws -> PreparePayResult2 { 625 let request = PreparePayForUri(talerPayUri: talerPayUri) 626 let response = try await sendRequest(request, viewHandles: viewHandles) 627 return response 628 } 629 630 nonisolated func confirmPay(_ transactionId: String, choiceIndex: Int?, viewHandles: Bool = false) 631 async throws -> ConfirmPayResult { 632 let request = ConfirmPayForUri(transactionId: transactionId, 633 choiceIndex: choiceIndex) 634 let response = try await sendRequest(request, viewHandles: viewHandles) 635 return response 636 } 637 }