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:
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) {