taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit d98079e12966d6f477e7a6b39a18157bb64c6929
parent 987c556dcf4fe5fd118f144cf01d172890fc4426
Author: Iván Ávalos <avalos@disroot.org>
Date:   Wed, 29 Jan 2025 17:40:35 +0100

add contract v0/v1 types and codecs

Diffstat:
Mpackages/taler-util/src/codec.ts | 31++++++++++++++++++++++++++++++-
Mpackages/taler-util/src/types-taler-merchant.ts | 323+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mpackages/taler-util/src/types-taler-wallet.ts | 16++++++++--------
Mpackages/taler-wallet-core/src/deposits.ts | 6+++---
Mpackages/taler-wallet-core/src/pay-merchant.ts | 10+++++-----
5 files changed, 354 insertions(+), 32 deletions(-)

diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts @@ -69,6 +69,17 @@ export interface Codec<V> { readonly decode: (x: any, c?: Context) => V; } +/** + * A codec built from an ObjectCodecBuilder object. + */ +export interface ObjectCodec<V> extends Codec<V> { + /** + * Get properties from builder. This method is only supported for codecs + * built from ObjectCodecBuilder. + */ + readonly getProps: () => Prop[]; +} + type SingletonRecord<K extends keyof any, V> = { [Y in K]: V }; interface Prop { @@ -102,6 +113,20 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> { } /** + * Concatenate properties from @a codec. + * + * @param other codec to concat properties from + * + * FIXME: do proper union of all `other' props. + */ + mixin<K extends keyof OutputType & string, V extends OutputType[K]>( + other: ObjectCodec<V> + ): ObjectCodecBuilder<OutputType, PartialOutputType & V> { + this.propList.push(...other.getProps()); + return this as any; + } + + /** * Define a deprecated property for the object. * * Deprecated properties won't be validated, their presence will @@ -128,7 +153,7 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> { * @param objectDisplayName name of the object that this codec operates on, * used in error messages. */ - build(objectDisplayName: string): Codec<PartialOutputType> { + build(objectDisplayName: string): ObjectCodec<PartialOutputType> { const propList = this.propList; const allowExtra = this._allowExtra; const deprecatedPros = this.deprecatedProps; @@ -177,6 +202,10 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> { } return obj as PartialOutputType; }, + + getProps(): Prop[] { + return propList; + } }; } } diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -32,9 +32,11 @@ import { AccountLimit, CoinEnvelope, ExchangeWireAccount, + ObjectCodec, PaytoString, buildCodecForUnion, codecForAccountLimit, + codecForAmountJson, codecForConstNumber, codecForConstString, codecForEither, @@ -56,6 +58,7 @@ import { BlindedRsaSignature, ClaimToken, CoinPublicKey, + Cs25519Point, CurrencySpecification, EddsaPublicKey, EddsaPublicKeyString, @@ -66,6 +69,7 @@ import { Integer, InternationalizedString, RelativeTime, + RsaPublicKey, RsaSignature, Timestamp, WireTransferIdentifierRawP, @@ -456,7 +460,7 @@ export interface Product { * Contract terms from a merchant. * FIXME: Add type field! */ -export interface MerchantContractTerms { +interface MerchantContractTermsCommon { // The hash of the merchant instance's wire details. h_wire: string; @@ -499,11 +503,6 @@ export interface MerchantContractTerms { // encoded in it (such as a short product identifier and timestamp). order_id: string; - // Total price for the transaction. - // The exchange will subtract deposit fees from that amount - // before transferring it to the merchant. - amount: string; - // Nonce generated by the wallet and echoed by the merchant // in this field when the proposal is generated. nonce: string; @@ -582,10 +581,6 @@ export interface MerchantContractTerms { // messages. fulfillment_message_i18n?: InternationalizedString; - // Maximum total deposit fee accepted by the merchant for this contract. - // Overrides defaults of the merchant instance. - max_fee: string; - // Extra data that is only interpreted by the merchant frontend. // Useful when the merchant needs to store extra information on a // contract without storing it separately in their database. @@ -600,6 +595,185 @@ export interface MerchantContractTerms { minimum_age?: Integer; } +export interface MerchantContractTermsV0 extends MerchantContractTermsCommon { + version?: 0; + + // Total price for the transaction. + // The exchange will subtract deposit fees from that amount + // before transferring it to the merchant. + amount: AmountString; + + // Maximum total deposit fee accepted by the merchant for this contract. + // Overrides defaults of the merchant instance. + max_fee: AmountString; +} + +export interface MerchantContractTermsV1 extends MerchantContractTermsCommon { + version: 1; + + // List of contract choices that the customer can select from. + // @since protocol **vSUBSCRIBE** + choices: MerchantContractChoice[]; + + // Map of storing metadata and issue keys of + // token families referenced in this contract. + // @since protocol **vSUBSCRIBE** + token_families: { [token_family_slug: string]: MerchantContractTokenFamily }; +} + +export type MerchantContractTerms = + | MerchantContractTermsV0 + | MerchantContractTermsV1; + +export interface MerchantContractChoice { + // Price to be paid for this choice. Could be 0. + // The price is in addition to other instruments, + // such as rations and tokens. + // The exchange will subtract deposit fees from that amount + // before transferring it to the merchant. + amount: AmountString; + + // List of inputs the wallet must provision (all of them) to + // satisfy the conditions for the contract. + inputs: MerchantContractInput[]; + + // List of outputs the merchant promises to yield (all of them) + // once the contract is paid. + outputs: MerchantContractOutput[]; + + // Maximum total deposit fee accepted by the merchant for this contract. + max_fee: AmountString; +} + +export type MerchantContractInput = MerchantContractInputToken; + +export interface MerchantContractInputToken { + type: "token"; + + // Slug of the token family in the + // token_families map on the order. + token_family_slug: string; + + // Number of tokens of this type required. + // Defaults to one if the field is not provided. + count?: Integer; +} + +export type MerchantContractOutput = + | MerchantContractOutputToken + | MerchantContractOutputTaxReceipt; + +export interface MerchantContractOutputToken { + type: "token"; + + // Slug of the token family in the + // 'token_families' map on the top-level. + token_family_slug: string; + + // Number of tokens to be issued. + // Defaults to one if the field is not provided. + count?: Integer; + + // Index of the public key for this output token + // in the ContractTokenFamily keys array. + key_index: Integer; +} + +export interface MerchantContractOutputTaxReceipt { + type: "tax-receipt"; + + // Array of base URLs of donation authorities that can be + // used to issue the tax receipts. The client must select one. + donau_urls: string[]; + + // Total amount that will be on the tax receipt. + // Optional, if missing the full amount will be on the receipt. + amount?: AmountString; + +} + +export interface MerchantContractTokenFamily { + // Human readable name of the token family. + name: string; + + // Human-readable description of the semantics of + // this token family (for display). + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions. + description_i18n?: { [lang_tag: string]: string }; + + // Public keys used to validate tokens issued by this token family. + keys: TokenIssuePublicKey[]; + + // Kind-specific information of the token + details: MerchantContractTokenDetails; + + // Must a wallet understand this token type to + // process contracts that use or issue it? + critical: boolean; +} + +export type TokenIssuePublicKey = + | TokenIssueRsaPublicKey + | TokenIssueCsPublicKey; + +export interface TokenIssueRsaPublicKey { + cipher: "RSA"; + + // RSA public key. + rsa_pub: RsaPublicKey; + + // Start time of this key's signatures validity period. + signature_validity_start: Timestamp; + + // End time of this key's signatures validity period. + signature_validity_end: Timestamp; +} + +export interface TokenIssueCsPublicKey { + cipher: "CS"; + + // CS public key. + cs_pub: Cs25519Point; + + // Start time of this key's signatures validity period. + signature_validity_start: Timestamp; + + // End time of this key's signatures validity period. + signature_validity_end: Timestamp; + +} + +export type MerchantContractTokenDetails = + | MerchantContractSubscriptionTokenDetails + | MerchantContractDiscountTokenDetails; + +export interface MerchantContractSubscriptionTokenDetails { + class: "subscription"; + + // Array of domain names where this subscription + // can be safely used (e.g. the issuer warrants that + // these sites will re-issue tokens of this type + // if the respective contract says so). May contain + // "*" for any domain or subdomain. + trusted_domains: string[]; +} + +export interface MerchantContractDiscountTokenDetails { + class: "discount"; + + // Array of domain names where this discount token + // is intended to be used. May contain "*" for any + // domain or subdomain. Users should be warned about + // sites proposing to consume discount tokens of this + // type that are not in this list that the merchant + // is accepting a coupon from a competitor and thus + // may be attaching different semantics (like get 20% + // discount for my competitors 30% discount token). + expected_domains: string[]; +} + /** * Refund permission in the format that the merchant gives it to us. */ @@ -690,8 +864,8 @@ export const codecForMerchantInfo = (): Codec<MerchantInfo> => .property("jurisdiction", codecOptional(codecForLocation())) .build("MerchantInfo"); -export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> => - buildCodecForObject<MerchantContractTerms>() +const codecForMerchantContractTermsCommon = (): ObjectCodec<MerchantContractTermsCommon> => + buildCodecForObject<MerchantContractTermsCommon>() .property("order_id", codecForString()) .property("fulfillment_url", codecOptional(codecForString())) .property("fulfillment_message", codecOptional(codecForString())) @@ -706,21 +880,140 @@ export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> => .property("summary", codecForString()) .property("summary_i18n", codecOptional(codecForInternationalizedString())) .property("nonce", codecForString()) - .property("amount", codecForAmountString()) .property("pay_deadline", codecForTimestamp) .property("refund_deadline", codecForTimestamp) .property("wire_transfer_deadline", codecForTimestamp) .property("timestamp", codecForTimestamp) .property("delivery_location", codecOptional(codecForLocation())) .property("delivery_date", codecOptional(codecForTimestamp)) - .property("max_fee", codecForAmountString()) .property("merchant", codecForMerchantInfo()) .property("merchant_pub", codecForString()) .property("exchanges", codecForList(codecForExchange())) .property("products", codecOptional(codecForList(codecForProduct()))) .property("extra", codecForAny()) .property("minimum_age", codecOptional(codecForNumber())) - .build("MerchantContractTerms"); + .build("TalerMerchantApi.ContractTermsCommon"); + +export const codecForMerchantContractTermsV0 = (): Codec<MerchantContractTermsV0> => + buildCodecForObject<MerchantContractTermsV0>() + .property("version", codecOptional(codecForConstNumber(0))) + .property("amount", codecForAmountString()) + .property("max_fee", codecForAmountString()) + .mixin(codecForMerchantContractTermsCommon()) + .build("TalerMerchantApi.ContractTermsV0"); + +export const codecForMerchantContractTermsV1 = (): Codec<MerchantContractTermsV1> => + buildCodecForObject<MerchantContractTermsV1>() + .property("version", codecForConstNumber(1)) + .property("choices", codecForList(codecForMerchantContractChoice())) + .property("token_families", codecForMap(codecForMerchantContractTokenFamily())) + .mixin(codecForMerchantContractTermsCommon()) + .build("TalerMerchantApi.ContractTermsV1"); + +export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> => + buildCodecForUnion<MerchantContractTerms>() + .discriminateOn("version") + .alternative(undefined, codecForMerchantContractTermsV0()) + .alternative(0, codecForMerchantContractTermsV0()) + .alternative(1, codecForMerchantContractTermsV1()) + .build("TalerMerchantApi.ContractTerms"); + +export const codecForMerchantContractChoice = (): Codec<MerchantContractChoice> => + buildCodecForObject<MerchantContractChoice>() + .property("amount", codecForAmountString()) + .property("inputs", codecForList(codecForMerchantContractInput())) + .property("outputs", codecForList(codecForMerchantContractOutput())) + .property("max_fee", codecForAmountString()) + .build("TalerMerchantApi.ContractChoice"); + +export const codecForMerchantContractInput = (): Codec<MerchantContractInput> => + buildCodecForUnion<MerchantContractInput>() + .discriminateOn("type") + .alternative("token", codecForMerchantContractInputToken()) + .build("TalerMerchantApi.ContractInput"); + +export const codecForMerchantContractInputToken = (): Codec<MerchantContractInputToken> => + buildCodecForObject<MerchantContractInputToken>() + .property("type", codecForConstString("token")) + .property("token_family_slug", codecForString()) + .property("count", codecOptional(codecForNumber())) + .build("TalerMerchantApi.ContractInputToken"); + +export const codecForMerchantContractOutput = (): Codec<MerchantContractOutput> => + buildCodecForUnion<MerchantContractOutput>() + .discriminateOn("type") + .alternative("token", codecForMerchantContractOutputToken()) + .alternative("tax-receipt", codecForMerchantContractOutputTaxReceipt()) + .build("TalerMerchantApi.ContractOutput"); + +export const codecForMerchantContractOutputToken = (): Codec<MerchantContractOutputToken> => + buildCodecForObject<MerchantContractOutputToken>() + .property("type", codecForConstString("token")) + .property("token_family_slug", codecForString()) + .property("count", codecOptional(codecForNumber())) + .property("key_index", codecForNumber()) + .build("TalerMerchantApi.ContractOutputToken"); + +export const codecForMerchantContractOutputTaxReceipt = (): Codec<MerchantContractOutputTaxReceipt> => + buildCodecForObject<MerchantContractOutputTaxReceipt>() + .property("type", codecForConstString("tax-receipt")) + .property("donau_urls", codecForList(codecForString())) + .property("amount", codecOptional(codecForAmountString())) + .build("TalerMerchantApi.ContractOutputTaxReceipt"); + +export const codecForMerchantContractTokenFamily = (): Codec<MerchantContractTokenFamily> => + buildCodecForObject<MerchantContractTokenFamily>() + .property("name", codecForString()) + .property("description", codecForString()) + .property("description_i18n", codecOptional(codecForInternationalizedString())) + .property("keys", codecForList(codecForTokenIssuePublicKey())) + .property("details", codecForMerchantContractTokenDetails()) + .property("critical", codecForBoolean()) + .build("TalerMerchantApi.ContractTokenFamily"); + +export const codecForTokenIssuePublicKey = (): Codec<TokenIssuePublicKey> => + buildCodecForUnion<TokenIssuePublicKey>() + .discriminateOn("cipher") + .alternative("RSA", codecForTokenIssueRsaPublicKey()) + .alternative("CS", codecForTokenIssueCsPublicKey()) + .build("TalerMerchantApi.TokenIssuePublicKey"); + +export const codecForTokenIssueRsaPublicKey = (): Codec<TokenIssueRsaPublicKey> => + buildCodecForObject<TokenIssueRsaPublicKey>() + .property("cipher", codecForConstString("RSA")) + .property("rsa_pub", codecForString()) + .property("signature_validity_start", codecForTimestamp) + .property("signature_validity_end", codecForTimestamp) + .build("TalerMerchantApi.TokenIssueRsaPublicKey"); + +export const codecForTokenIssueCsPublicKey = (): Codec<TokenIssueCsPublicKey> => + buildCodecForObject<TokenIssueCsPublicKey>() + .property("cipher", codecForConstString("CS")) + .property("cs_pub", codecForString()) + .property("signature_validity_start", codecForTimestamp) + .property("signature_validity_end", codecForTimestamp) + .build("TalerMerchantApi.TokenIssueRsaPublicKey"); + +export const codecForMerchantContractTokenDetails = (): Codec<MerchantContractTokenDetails> => + buildCodecForUnion<MerchantContractTokenDetails>() + .discriminateOn("class") + .alternative("subscription", codecForMerchantContractSubscriptionTokenDetails()) + .alternative("discount", codecForMerchantContractDiscountTokenDetails()) + .build("TalerMerchantApi.ContractTokenDetails"); + +export const codecForMerchantContractSubscriptionTokenDetails = + (): Codec<MerchantContractSubscriptionTokenDetails> => + buildCodecForObject<MerchantContractSubscriptionTokenDetails>() + .property("class", codecForConstString("subscription")) + .property("trusted_domains", codecForList(codecForString())) + .build("TalerMerchantApi.ContractSubscriptionTokenDetails"); + +export const codecForMerchantContractDiscountTokenDetails = + (): Codec<MerchantContractDiscountTokenDetails> => + buildCodecForObject<MerchantContractDiscountTokenDetails>() + .property("class", codecForConstString("discount")) + .property("expected_domains", codecForList(codecForString())) + .build("TalerMerchantApi.ContractDiscountTokenDetails"); export interface TalerMerchantConfigResponse { // libtool-style representation of the Merchant protocol version, see diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -84,9 +84,9 @@ import { codecForPeerContractTerms, } from "./types-taler-exchange.js"; import { - MerchantContractTerms, + MerchantContractTermsV0, MerchantInfo, - codecForMerchantContractTerms, + codecForMerchantContractTermsV0, } from "./types-taler-merchant.js"; import { BackupRecovery } from "./types-taler-sync.js"; import { @@ -663,7 +663,7 @@ export enum ConfirmPayResultType { */ export interface ConfirmPayResultDone { type: ConfirmPayResultType.Done; - contractTerms: MerchantContractTerms; + contractTerms: MerchantContractTermsV0; transactionId: TransactionIdStr; } @@ -694,7 +694,7 @@ export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> => buildCodecForObject<ConfirmPayResultDone>() .property("type", codecForConstString(ConfirmPayResultType.Done)) .property("transactionId", codecForTransactionIdStr()) - .property("contractTerms", codecForMerchantContractTerms()) + .property("contractTerms", codecForMerchantContractTermsV0()) .build("ConfirmPayResultDone"); export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> => @@ -735,7 +735,7 @@ export const codecForPreparePayResultPaymentPossible = buildCodecForObject<PreparePayResultPaymentPossible>() .property("amountEffective", codecForAmountString()) .property("amountRaw", codecForAmountString()) - .property("contractTerms", codecForMerchantContractTerms()) + .property("contractTerms", codecForMerchantContractTermsV0()) .property("transactionId", codecForTransactionIdStr()) .property("contractTermsHash", codecForString()) .property("scopes", codecForList(codecForScopeInfo())) @@ -890,7 +890,7 @@ export interface PreparePayResultPaymentPossible { transactionId: TransactionIdStr; - contractTerms: MerchantContractTerms; + contractTerms: MerchantContractTermsV0; /** * Scopes involved in this transaction. @@ -924,7 +924,7 @@ export interface PreparePayResultInsufficientBalance { */ scopes: ScopeInfo[]; - contractTerms: MerchantContractTerms; + contractTerms: MerchantContractTermsV0; amountRaw: AmountString; @@ -938,7 +938,7 @@ export interface PreparePayResultAlreadyConfirmed { transactionId: TransactionIdStr; - contractTerms: MerchantContractTerms; + contractTerms: MerchantContractTermsV0; paid: boolean; diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -41,7 +41,7 @@ import { HttpStatusCode, KycAuthTransferInfo, Logger, - MerchantContractTerms, + MerchantContractTermsV0, RefreshReason, ScopeInfo, SelectedProspectiveCoin, @@ -1466,7 +1466,7 @@ async function processDepositGroupPendingDeposit( if (!contractTermsRec) { throw Error("contract terms for deposit not found in database"); } - const contractTerms: MerchantContractTerms = + const contractTerms: MerchantContractTermsV0 = contractTermsRec.contractTermsRaw; const contractData = extractContractData( contractTermsRec.contractTermsRaw, @@ -2085,7 +2085,7 @@ export async function createDepositGroup( const noncePair = await wex.cryptoApi.createEddsaKeypair({}); const wireSalt = encodeCrock(getRandomBytes(16)); const wireHash = hashWire(req.depositPaytoUri, wireSalt); - const contractTerms: MerchantContractTerms = { + const contractTerms: MerchantContractTermsV0 = { exchanges: exchangeInfos.map((x) => ({ master_pub: x.master_pub, priority: 1, diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -36,7 +36,7 @@ import { CheckPayTemplateReponse, CheckPayTemplateRequest, codecForAbortResponse, - codecForMerchantContractTerms, + codecForMerchantContractTermsV0, codecForMerchantOrderStatusPaid, codecForMerchantPayResponse, codecForPostOrderResponse, @@ -59,7 +59,7 @@ import { makePendingOperationFailedError, makeTalerErrorDetail, MerchantCoinRefundStatus, - MerchantContractTerms, + MerchantContractTermsV0, MerchantPayResponse, MerchantUsingTemplateDetails, NotificationType, @@ -855,7 +855,7 @@ export async function expectProposalDownload( } export function extractContractData( - parsedContractTerms: MerchantContractTerms, + parsedContractTerms: MerchantContractTermsV0, contractTermsHash: string, merchantSig: string, ): WalletContractData { @@ -990,10 +990,10 @@ async function processDownloadProposal( proposalResp.contract_terms, ); - let parsedContractTerms: MerchantContractTerms; + let parsedContractTerms: MerchantContractTermsV0; try { - parsedContractTerms = codecForMerchantContractTerms().decode( + parsedContractTerms = codecForMerchantContractTermsV0().decode( proposalResp.contract_terms, ); } catch (e) {