commit be8960eaee144abd748d10e78fb5ebd50d6911ca
parent fc12a3230cc4c6e5a7371d7ba7b9a08e104b1a5e
Author: Iván Ávalos <avalos@disroot.org>
Date: Tue, 29 Apr 2025 17:07:10 +0200
wallet-core: refactor contract amount extraction
Diffstat:
2 files changed, 127 insertions(+), 172 deletions(-)
diff --git a/packages/taler-util/src/contract-terms.ts b/packages/taler-util/src/contract-terms.ts
@@ -26,6 +26,10 @@ import {
stringToBytes,
} from "./taler-crypto.js";
import {
+ AmountString,
+ Integer,
+} from "./types-taler-common.js";
+import {
MerchantContractTerms,
MerchantContractTokenKind,
MerchantContractVersion,
@@ -266,4 +270,52 @@ export namespace ContractTermsUtil {
const bytes = stringToBytes(canon);
return encodeCrock(hash(bytes));
}
+
+ /**
+ * Extract raw amount and max fee.
+ */
+ export function extractAmounts(
+ contractTerms: MerchantContractTerms,
+ choiceIndex: Integer | undefined,
+ ): {
+ available: true,
+ amountRaw: AmountString,
+ maxFee: AmountString,
+ } | {
+ available: false,
+ amountRaw: undefined,
+ maxFee: undefined,
+ } {
+ let amountRaw: AmountString;
+ let maxFee: AmountString;
+ switch (contractTerms.version) {
+ case undefined:
+ case MerchantContractVersion.V0:
+ amountRaw = contractTerms.amount;
+ maxFee = contractTerms.max_fee;
+ break;
+ case MerchantContractVersion.V1:
+ if (choiceIndex === undefined) {
+ logger.trace("choice index not specified for contract v1");
+ return {
+ available: false,
+ amountRaw: undefined,
+ maxFee: undefined,
+ };
+ }
+ if (choiceIndex in contractTerms.choices)
+ throw Error(`invalid choice index ${choiceIndex}`);
+ amountRaw = contractTerms.choices[choiceIndex].amount;
+ maxFee = contractTerms.choices[choiceIndex].max_fee;
+ break;
+ default:
+ assertUnreachable(contractTerms);
+ }
+
+ return {
+ available: true,
+ amountRaw,
+ maxFee,
+ };
+ }
}
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -938,6 +938,7 @@ export async function expectProposalDownloadInTx(
if (!contractTerms) {
throw Error("contract terms not found");
}
+
return {
contractData: extractContractData(
contractTerms.contractTermsRaw,
@@ -1711,25 +1712,12 @@ async function handleInsufficientFunds(
proposal,
);
- let amount: AmountString;
- let maxFee: AmountString;
- switch (contractData.version) {
- case undefined:
- case MerchantContractVersion.V0:
- amount = contractData.amount;
- maxFee = contractData.max_fee;
- break;
- case MerchantContractVersion.V1:
- const index = p.choiceIndex;
- if (index === undefined)
- throw Error("choice index not specified for contract v1");
- if (index in contractData.choices)
- throw Error(`invalid choice index ${index}`);
- amount = contractData.choices[index].amount;
- maxFee = contractData.choices[index].max_fee;
- break;
- default:
- assertUnreachable(contractData);
+ const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts(
+ contractData,
+ p.choiceIndex,
+ );
+ if (!available) {
+ throw Error("choice index not specified for contract v1");
}
if (payCoinSelection) {
@@ -1751,7 +1739,7 @@ async function handleInsufficientFunds(
})),
},
restrictWireMethod: contractData.wire_method,
- contractTermsAmount: Amounts.parseOrThrow(amount),
+ contractTermsAmount: Amounts.parseOrThrow(amountRaw),
depositFeeLimit: Amounts.parseOrThrow(maxFee),
prevPayCoins,
requiredMinimumAge: contractData.minimum_age,
@@ -2035,22 +2023,12 @@ async function checkPaymentByProposalId(
const download = await expectProposalDownload(wex, purchase);
const contractData = download.contractData;
- let amount: AmountString;
- switch (contractData.version) {
- case undefined:
- case MerchantContractVersion.V0:
- amount = contractData.amount;
- break;
- case MerchantContractVersion.V1:
- const index = purchase.choiceIndex;
- if (index === undefined)
- throw Error("choice index not specified for contract v1");
- if (index in contractData.choices)
- throw Error(`invalid choice index ${index}`);
- amount = contractData.choices[index].amount;
- break;
- default:
- assertUnreachable(contractData);
+ const { available, amountRaw } = ContractTermsUtil.extractAmounts(
+ contractData,
+ purchase.choiceIndex,
+ );
+ if (!available) {
+ throw Error("choice index not specified for contract v1");
}
return {
@@ -2058,7 +2036,7 @@ async function checkPaymentByProposalId(
contractTerms: download.contractTermsRaw,
contractTermsHash: download.contractData.contractTermsHash,
paid: true,
- amountRaw: Amounts.stringify(amount),
+ amountRaw: Amounts.stringify(amountRaw),
amountEffective: purchase.payInfo
? Amounts.stringify(purchase.payInfo.totalPayCost)
: undefined,
@@ -2069,22 +2047,12 @@ async function checkPaymentByProposalId(
} else if (!purchase.timestampFirstSuccessfulPay) {
const download = await expectProposalDownload(wex, purchase);
- let amount: AmountString;
- switch (download.contractData.version) {
- case undefined:
- case MerchantContractVersion.V0:
- amount = download.contractData.amount;
- break;
- case MerchantContractVersion.V1:
- const index = purchase.choiceIndex;
- if (index === undefined)
- throw Error(`choice index not specified for contract v1`);
- if (!(index in download.contractData.choices))
- throw Error(`invalid choice index ${index}`);
- amount = download.contractData.choices[index].amount;
- break;
- default:
- assertUnreachable(download.contractData);
+ const { available, amountRaw } = ContractTermsUtil.extractAmounts(
+ contractData,
+ purchase.choiceIndex,
+ );
+ if (!available) {
+ throw Error("choice index not specified for contract v1");
}
return {
@@ -2092,7 +2060,7 @@ async function checkPaymentByProposalId(
contractTerms: download.contractTermsRaw,
contractTermsHash: download.contractData.contractTermsHash,
paid: purchase.purchaseStatus === PurchaseStatus.FailedPaidByOther,
- amountRaw: Amounts.stringify(amount),
+ amountRaw: Amounts.stringify(amountRaw),
amountEffective: purchase.payInfo
? Amounts.stringify(purchase.payInfo.totalPayCost)
: undefined,
@@ -2104,22 +2072,12 @@ async function checkPaymentByProposalId(
const paid = isPurchasePaid(purchase);
const download = await expectProposalDownload(wex, purchase);
- let amount: AmountString;
- switch (download.contractData.version) {
- case undefined:
- case MerchantContractVersion.V0:
- amount = download.contractData.amount;
- break;
- case MerchantContractVersion.V1:
- const index = purchase.choiceIndex;
- if (index === undefined)
- throw Error("choice index not specified for contract v1");
- if (!(index in download.contractData.choices))
- throw Error(`invalid choice index ${index}`);
- amount = download.contractData.choices[index].amount;
- break;
- default:
- assertUnreachable(download.contractData);
+ const { available, amountRaw } = ContractTermsUtil.extractAmounts(
+ contractData,
+ purchase.choiceIndex,
+ );
+ if (!available) {
+ throw Error("choice index not specified for contract v1");
}
return {
@@ -2127,7 +2085,7 @@ async function checkPaymentByProposalId(
contractTerms: download.contractTermsRaw,
contractTermsHash: download.contractData.contractTermsHash,
paid,
- amountRaw: Amounts.stringify(amount),
+ amountRaw: Amounts.stringify(amountRaw),
amountEffective: purchase.payInfo
? Amounts.stringify(purchase.payInfo.totalPayCost)
: undefined,
@@ -2576,30 +2534,23 @@ export async function getChoicesForPayment(
}
for (let i = 0; i < tokenSels.length; i++) {
- const tokenSelection = tokenSels[i];
+ const choiceIndex = i;
+ const tokenSelection = tokenSels[choiceIndex];
logger.trace("token selection result", tokenSelection);
let balanceDetails: PaymentInsufficientBalanceDetails | undefined =
undefined;
- let amount: AmountString;
- let maxFee: AmountString;
- switch (contractTerms.version) {
- case undefined:
- case MerchantContractVersion.V0:
- amount = contractTerms.amount;
- maxFee = contractTerms.max_fee;
- break;
- case MerchantContractVersion.V1:
- amount = contractTerms.choices[i].amount;
- maxFee = contractTerms.choices[i].max_fee;
- break;
- default:
- assertUnreachable(contractTerms);
+ const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts(
+ contractTerms,
+ choiceIndex,
+ );
+ if (!available) {
+ throw Error("choice index not specified for contract v1");
}
let amountEffective: AmountJson | undefined = undefined;
- const currency = Amounts.currencyOf(amount);
+ const currency = Amounts.currencyOf(amountRaw);
const selectCoinsResult = await selectPayCoinsInTx(wex, tx, {
restrictExchanges: {
@@ -2610,7 +2561,7 @@ export async function getChoicesForPayment(
})),
},
restrictWireMethod: contractTerms.wire_method,
- contractTermsAmount: Amounts.parseOrThrow(amount),
+ contractTermsAmount: Amounts.parseOrThrow(amountRaw),
depositFeeLimit: Amounts.parseOrThrow(maxFee),
prevPayCoins: [],
requiredMinimumAge: contractTerms.minimum_age,
@@ -2654,14 +2605,14 @@ export async function getChoicesForPayment(
) {
choice = {
status: ChoiceSelectionDetailType.InsufficientBalance,
- amountRaw: amount,
+ amountRaw: amountRaw,
balanceDetails: balanceDetails,
tokenDetails: tokenSelection.details,
};
} else {
choice = {
status: ChoiceSelectionDetailType.PaymentPossible,
- amountRaw: amount,
+ amountRaw: amountRaw,
amountEffective: Amounts.stringify(amountEffective!),
tokenDetails: tokenSelection.details,
};
@@ -2834,30 +2785,17 @@ export async function confirmPay(
logger.trace("confirmPay: purchase record does not exist yet");
- let amount: AmountString;
- let maxFee: AmountString;
- let currency: string;
const contractData = d.contractData;
- switch (contractData.version) {
- case undefined:
- case MerchantContractVersion.V0:
- amount = contractData.amount;
- maxFee = contractData.max_fee;
- currency = Amounts.currencyOf(amount);
- break;
- case MerchantContractVersion.V1:
- if (choiceIndex === undefined)
- throw Error("choice index not specified for contract v1");
- if (!(choiceIndex in contractData.choices))
- throw Error(`invalid choice index ${choiceIndex}`);
- amount = contractData.choices[choiceIndex].amount;
- maxFee = contractData.choices[choiceIndex].max_fee;
- currency = Amounts.currencyOf(amount);
- break;
- default:
- assertUnreachable(contractData);
+ const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts(
+ contractData,
+ choiceIndex,
+ );
+ if (!available) {
+ throw Error("choice index not specified for contract v1");
}
+ const currency = Amounts.currencyOf(amountRaw);
+
let sessionId: string | undefined;
if (sessionIdOverride) {
sessionId = sessionIdOverride;
@@ -2905,7 +2843,7 @@ export async function confirmPay(
})),
},
restrictWireMethod: contractData.wire_method,
- contractTermsAmount: Amounts.parseOrThrow(amount),
+ contractTermsAmount: Amounts.parseOrThrow(amountRaw),
depositFeeLimit: Amounts.parseOrThrow(maxFee),
prevPayCoins: [],
requiredMinimumAge: contractData.minimum_age,
@@ -3187,29 +3125,16 @@ async function processPurchasePay(
const contractData = download.contractData;
const choiceIndex = purchase.choiceIndex;
- let amount: AmountString;
- let maxFee: AmountString;
- let currency: string;
- switch (contractData.version) {
- case undefined:
- case MerchantContractVersion.V0:
- amount = contractData.amount;
- maxFee = contractData.max_fee;
- currency = Amounts.currencyOf(amount);
- break;
- case MerchantContractVersion.V1:
- if (choiceIndex === undefined)
- throw Error("choice index not specified for contract v1");
- if (!(choiceIndex in contractData.choices))
- throw Error(`invalid choice index ${choiceIndex}`);
- amount = contractData.choices[choiceIndex].amount;
- maxFee = contractData.choices[choiceIndex].max_fee;
- currency = Amounts.currencyOf(amount);
- break;
- default:
- assertUnreachable(contractData);
+ const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts(
+ contractData,
+ choiceIndex,
+ );
+ if (!available) {
+ throw Error("choice index not specified for contract v1");
}
+ const currency = Amounts.currencyOf(amountRaw);
+
if (!payInfo.payCoinSelection) {
const selectCoinsResult = await selectPayCoins(wex, {
restrictExchanges: {
@@ -3220,7 +3145,7 @@ async function processPurchasePay(
})),
},
restrictWireMethod: contractData.wire_method,
- contractTermsAmount: Amounts.parseOrThrow(amount),
+ contractTermsAmount: Amounts.parseOrThrow(amountRaw),
depositFeeLimit: Amounts.parseOrThrow(maxFee),
prevPayCoins: [],
requiredMinimumAge: contractData.minimum_age,
@@ -4159,23 +4084,12 @@ async function processPurchaseAutoRefund(
const download = await expectProposalDownload(wex, purchase);
const contractData = download.contractData;
-
- let amount: AmountString;
- switch (contractData.version) {
- case undefined:
- case MerchantContractVersion.V0:
- amount = contractData.amount;
- break;
- case MerchantContractVersion.V1:
- const index = purchase.choiceIndex;
- if (index === undefined)
- throw Error("choice index not specified for contract v1");
- if (index in contractData.choices)
- throw Error(`invalid choice index ${index}`);
- amount = contractData.choices[index].amount;
- break;
- default:
- assertUnreachable(contractData);
+ const { available, amountRaw } = ContractTermsUtil.extractAmounts(
+ contractData,
+ purchase.choiceIndex,
+ );
+ if (!available) {
+ throw Error("choice index not specified for contract v1");
}
const noAutoRefundOrExpired =
@@ -4192,7 +4106,7 @@ async function processPurchaseAutoRefund(
const refunds = await tx.refundGroups.indexes.byProposalId.getAll(
purchase.proposalId,
);
- const am = Amounts.parseOrThrow(amount);
+ const am = Amounts.parseOrThrow(amountRaw);
return refunds.reduce((prev, cur) => {
if (
cur.status === RefundGroupStatus.Done ||
@@ -4205,7 +4119,7 @@ async function processPurchaseAutoRefund(
},
);
- const fullyRefunded = Amounts.cmp(amount, totalKnownRefund) <= 0;
+ const fullyRefunded = Amounts.cmp(amountRaw, totalKnownRefund) <= 0;
// We stop with the auto-refund state when the auto-refund period
// is over or the product is already fully refunded.
@@ -4635,26 +4549,15 @@ async function storeRefunds(
const download = await expectProposalDownload(wex, purchase);
const contractData = download.contractData;
-
- let amount: AmountString;
- switch (contractData.version) {
- case undefined:
- case MerchantContractVersion.V0:
- amount = contractData.amount;
- break;
- case MerchantContractVersion.V1:
- const index = purchase.choiceIndex;
- if (index === undefined)
- throw Error("choice index not specified for contract v1");
- if (index in contractData.choices)
- throw Error(`invalid choice index ${index}`);
- amount = contractData.choices[index].amount;
- break;
- default:
- assertUnreachable(contractData);
+ const { available, amountRaw } = ContractTermsUtil.extractAmounts(
+ contractData,
+ purchase.choiceIndex,
+ );
+ if (!available) {
+ throw Error("choice index not specified for contract v1");
}
- const currency = Amounts.currencyOf(amount);
+ const currency = Amounts.currencyOf(amountRaw);
const transitions: {
transactionId: string;
@@ -4851,7 +4754,7 @@ async function storeRefunds(
await createRefreshGroup(
wex,
tx,
- Amounts.currencyOf(amount),
+ Amounts.currencyOf(amountRaw),
refreshCoins,
RefreshReason.Refund,
// Since refunds are really just pseudo-transactions,