diff options
-rw-r--r-- | packages/taler-util/src/walletTypes.ts | 17 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/refresh.ts | 5 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/reserves.ts | 17 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/tip.ts | 3 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/withdraw.ts | 114 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 17 |
6 files changed, 105 insertions, 68 deletions
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index 552087fb8..818ba37fe 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -212,6 +212,12 @@ export interface CreateReserveRequest { * URL to fetch the withdraw status from the bank. */ bankWithdrawStatusUrl?: string; + + /** + * Forced denomination selection for the first withdrawal + * from this reserve, only used for testing. + */ + forcedDenomSel?: ForcedDenomSel; } export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> => @@ -727,6 +733,7 @@ export interface GetWithdrawalDetailsForAmountRequest { export interface AcceptBankIntegratedWithdrawalRequest { talerWithdrawUri: string; exchangeBaseUrl: string; + forcedDenomSel?: ForcedDenomSel; } export const codecForAcceptBankIntegratedWithdrawalRequest = @@ -734,6 +741,7 @@ export const codecForAcceptBankIntegratedWithdrawalRequest = buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>() .property("exchangeBaseUrl", codecForString()) .property("talerWithdrawUri", codecForString()) + .property("forcedDenomSel", codecForAny()) .build("AcceptBankIntegratedWithdrawalRequest"); export const codecForGetWithdrawalDetailsForAmountRequest = @@ -1134,6 +1142,9 @@ export const codecForImportDbRequest = (): Codec<ImportDb> => .property("dump", codecForAny()) .build("ImportDbRequest"); - - -
\ No newline at end of file +export interface ForcedDenomSel { + denoms: { + value: AmountString; + count: number; + }[]; +} diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 762023d2e..cf292061f 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -106,11 +106,12 @@ export function getTotalRefreshCost( amountLeft, refreshedDenom.feeRefresh, ).amount; + const denomMap = Object.fromEntries(denoms.map((x) => [x.denomPubHash, x])); const withdrawDenoms = selectWithdrawalDenominations(withdrawAmount, denoms); const resultingAmount = Amounts.add( Amounts.getZero(withdrawAmount.currency), ...withdrawDenoms.selectedDenoms.map( - (d) => Amounts.mult(d.denom.value, d.count).amount, + (d) => Amounts.mult(denomMap[d.denomPubHash].value, d.count).amount, ), ).amount; const totalCost = Amounts.sub(amountLeft, resultingAmount).amount; @@ -277,7 +278,7 @@ async function refreshCreateSession( sessionSecretSeed: sessionSecretSeed, newDenoms: newCoinDenoms.selectedDenoms.map((x) => ({ count: x.count, - denomPubHash: x.denom.denomPubHash, + denomPubHash: x.denomPubHash, })), amountRefreshOutput: newCoinDenoms.totalCoinValue, }; diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index 38a7386b2..91c19fbf0 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -37,6 +37,8 @@ import { TalerErrorDetail, AbsoluteTime, URL, + AmountString, + ForcedDenomSel, } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; import { @@ -68,7 +70,6 @@ import { updateExchangeFromUrl, } from "./exchanges.js"; import { - denomSelectionInfoToState, getBankWithdrawalInfo, getCandidateWithdrawalDenoms, processWithdrawGroup, @@ -180,8 +181,7 @@ export async function createReserve( await updateWithdrawalDenoms(ws, canonExchange); const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange); - const denomSelInfo = selectWithdrawalDenominations(req.amount, denoms); - const initialDenomSel = denomSelectionInfoToState(denomSelInfo); + const initialDenomSel = selectWithdrawalDenominations(req.amount, denoms); const reserveRecord: ReserveRecord = { instructedAmount: req.amount, @@ -630,7 +630,7 @@ async function updateReserve( amountReservePlus, amountReserveMinus, ).amount; - const denomSelInfo = selectWithdrawalDenominations( + const denomSel = selectWithdrawalDenominations( remainingAmount, denoms, ); @@ -639,11 +639,11 @@ async function updateReserve( `Remaining unclaimed amount in reseve is ${Amounts.stringify( remainingAmount, )} and can be withdrawn with ${ - denomSelInfo.selectedDenoms.length + denomSel.selectedDenoms.length } coins`, ); - if (denomSelInfo.selectedDenoms.length === 0) { + if (denomSel.selectedDenoms.length === 0) { newReserve.reserveStatus = ReserveRecordStatus.Dormant; newReserve.operationStatus = OperationStatus.Finished; delete newReserve.lastError; @@ -669,7 +669,7 @@ async function updateReserve( timestampStart: AbsoluteTime.toTimestamp(AbsoluteTime.now()), retryInfo: resetRetryInfo(), lastError: undefined, - denomsSel: denomSelectionInfoToState(denomSelInfo), + denomsSel: denomSel, secretSeed: encodeCrock(getRandomBytes(64)), denomSelUid: encodeCrock(getRandomBytes(32)), operationStatus: OperationStatus.Pending, @@ -755,6 +755,9 @@ export async function createTalerWithdrawReserve( ws: InternalWalletState, talerWithdrawUri: string, selectedExchange: string, + options: { + forcedDenomSel?: ForcedDenomSel; + } = {}, ): Promise<AcceptWithdrawalResponse> { await updateExchangeFromUrl(ws, selectedExchange); const withdrawInfo = await getBankWithdrawalInfo(ws.http, talerWithdrawUri); diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 8bf85fe99..c0dcae911 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -56,7 +56,6 @@ import { updateWithdrawalDenoms, getCandidateWithdrawalDenoms, selectWithdrawalDenominations, - denomSelectionInfoToState, } from "./withdraw.js"; import { getHttpResponseErrorDetails, @@ -133,7 +132,7 @@ export async function prepareTip( tipAmountEffective: selectedDenoms.totalCoinValue, retryInfo: resetRetryInfo(), lastError: undefined, - denomsSel: denomSelectionInfoToState(selectedDenoms), + denomsSel: selectedDenoms, pickedUpTimestamp: undefined, secretSeed, denomSelUid, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index e7dcd0784..6d45599dc 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -31,6 +31,7 @@ import { durationFromSpec, ExchangeListItem, ExchangeWithdrawRequest, + ForcedDenomSel, LibtoolVersion, Logger, NotificationType, @@ -68,6 +69,7 @@ import { HttpRequestLibrary, readSuccessResponseJsonOrThrow, } from "../util/http.js"; +import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { resetRetryInfo, RetryInfo, @@ -85,21 +87,6 @@ import { guardOperationException } from "./common.js"; const logger = new Logger("operations/withdraw.ts"); /** - * FIXME: Eliminate this in favor of DenomSelectionState. - */ -interface DenominationSelectionInfo { - totalCoinValue: AmountJson; - totalWithdrawCost: AmountJson; - selectedDenoms: { - /** - * How many times do we withdraw this denomination? - */ - count: number; - denom: DenominationRecord; - }[]; -} - -/** * Information about what will happen when creating a reserve. * * Sent to the wallet frontend to be rendered and shown to the user. @@ -122,7 +109,7 @@ export interface ExchangeWithdrawDetails { /** * Selected denominations for withdraw. */ - selectedDenoms: DenominationSelectionInfo; + selectedDenoms: DenomSelectionState; /** * Does the wallet know about an auditor for @@ -213,12 +200,12 @@ export function isWithdrawableDenom(d: DenominationRecord): boolean { export function selectWithdrawalDenominations( amountAvailable: AmountJson, denoms: DenominationRecord[], -): DenominationSelectionInfo { +): DenomSelectionState { let remaining = Amounts.copy(amountAvailable); const selectedDenoms: { count: number; - denom: DenominationRecord; + denomPubHash: string; }[] = []; let totalCoinValue = Amounts.getZero(amountAvailable.currency); @@ -248,7 +235,7 @@ export function selectWithdrawalDenominations( ).amount; selectedDenoms.push({ count, - denom: d, + denomPubHash: d.denomPubHash, }); } @@ -262,9 +249,7 @@ export function selectWithdrawalDenominations( `selected withdrawal denoms for ${Amounts.stringify(totalCoinValue)}`, ); for (const sd of selectedDenoms) { - logger.trace( - `denom_pub_hash=${sd.denom.denomPubHash}, count=${sd.count}`, - ); + logger.trace(`denom_pub_hash=${sd.denomPubHash}, count=${sd.count}`); } logger.trace("(end of withdrawal denom list)"); } @@ -276,6 +261,56 @@ export function selectWithdrawalDenominations( }; } +export function selectForcedWithdrawalDenominations( + amountAvailable: AmountJson, + denoms: DenominationRecord[], + forcedDenomSel: ForcedDenomSel, +): DenomSelectionState { + let remaining = Amounts.copy(amountAvailable); + + const selectedDenoms: { + count: number; + denomPubHash: string; + }[] = []; + + let totalCoinValue = Amounts.getZero(amountAvailable.currency); + let totalWithdrawCost = Amounts.getZero(amountAvailable.currency); + + denoms = denoms.filter(isWithdrawableDenom); + denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); + + for (const fds of forcedDenomSel.denoms) { + const count = fds.count; + const denom = denoms.find((x) => { + return Amounts.cmp(x.value, fds.value) == 0; + }); + if (!denom) { + throw Error( + `unable to find denom for forced selection (value ${fds.value})`, + ); + } + const cost = Amounts.add(denom.value, denom.feeWithdraw).amount; + totalCoinValue = Amounts.add( + totalCoinValue, + Amounts.mult(denom.value, count).amount, + ).amount; + totalWithdrawCost = Amounts.add( + totalWithdrawCost, + Amounts.mult(cost, count).amount, + ).amount; + selectedDenoms.push({ + count, + denomPubHash: denom.denomPubHash, + }); + } + + return { + selectedDenoms, + totalCoinValue, + totalWithdrawCost, + }; +} + /** * Get information about a withdrawal from * a taler://withdraw URI by asking the bank. @@ -695,21 +730,6 @@ async function processPlanchetVerifyAndStoreCoin( } } -export function denomSelectionInfoToState( - dsi: DenominationSelectionInfo, -): DenomSelectionState { - return { - selectedDenoms: dsi.selectedDenoms.map((x) => { - return { - count: x.count, - denomPubHash: x.denom.denomPubHash, - }; - }), - totalCoinValue: dsi.totalCoinValue, - totalWithdrawCost: dsi.totalWithdrawCost, - }; -} - /** * Make sure that denominations that currently can be used for withdrawal * are validated, and the result of validation is stored in the database. @@ -1006,11 +1026,21 @@ export async function getExchangeWithdrawalInfo( exchange, ); - let earliestDepositExpiration = - selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit; + let earliestDepositExpiration: TalerProtocolTimestamp | undefined; for (let i = 1; i < selectedDenoms.selectedDenoms.length; i++) { - const expireDeposit = - selectedDenoms.selectedDenoms[i].denom.stampExpireDeposit; + const ds = selectedDenoms.selectedDenoms[i]; + // FIXME: Do in one transaction! + const denom = await ws.db + .mktx((x) => ({ denominations: x.denominations })) + .runReadOnly(async (tx) => { + return ws.getDenomInfo(ws, tx, exchangeBaseUrl, ds.denomPubHash); + }); + checkDbInvariant(!!denom); + const expireDeposit = denom.stampExpireDeposit; + if (!earliestDepositExpiration) { + earliestDepositExpiration = expireDeposit; + continue; + } if ( AbsoluteTime.cmp( AbsoluteTime.fromTimestamp(expireDeposit), @@ -1021,6 +1051,8 @@ export async function getExchangeWithdrawalInfo( } } + checkLogicInvariant(!!earliestDepositExpiration); + const possibleDenoms = await ws.db .mktx((x) => ({ denominations: x.denominations })) .runReadOnly(async (tx) => { diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index b0bd2a2cb..673a86167 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -598,18 +598,6 @@ async function getExchanges( return { exchanges }; } -async function acceptWithdrawal( - ws: InternalWalletState, - talerWithdrawUri: string, - selectedExchange: string, -): Promise<AcceptWithdrawalResponse> { - try { - return createTalerWithdrawReserve(ws, talerWithdrawUri, selectedExchange); - } finally { - ws.latch.trigger(); - } -} - /** * Inform the wallet that the status of a reserve has changed (e.g. due to a * confirmation from the bank.). @@ -849,10 +837,13 @@ async function dispatchRequestInternal( case "acceptBankIntegratedWithdrawal": { const req = codecForAcceptBankIntegratedWithdrawalRequest().decode(payload); - return await acceptWithdrawal( + return await createTalerWithdrawReserve( ws, req.talerWithdrawUri, req.exchangeBaseUrl, + { + forcedDenomSel: req.forcedDenomSel, + }, ); } case "getExchangeTos": { |