taler-typescript-core

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

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:
Mpackages/taler-util/src/contract-terms.ts | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/pay-merchant.ts | 247++++++++++++++++++++++++-------------------------------------------------------
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,