taler-typescript-core

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

commit d1cda666c629bfc3b5bc0a8f2039f5eaa80a6964
parent c3893d9a80e201e4b64724794bd406e1999e2418
Author: Florian Dold <florian@dold.me>
Date:   Wed,  4 Dec 2024 22:46:13 +0100

wallet-core: implement scope restriction and account restriction reporting for getMaxDepositAmount

Diffstat:
Mpackages/taler-util/src/types-taler-wallet.ts | 20++++++++++++++++++++
Mpackages/taler-wallet-core/src/coinSelection.ts | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mpackages/taler-wallet-core/src/deposits.ts | 18++----------------
Mpackages/taler-wallet-core/src/pay-merchant.ts | 156++++++++++++++++++++++++++++++++-----------------------------------------------
4 files changed, 151 insertions(+), 120 deletions(-)

diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -238,14 +238,27 @@ export const codecForConvertAmountRequest = .build("ConvertAmountRequest"); export interface GetMaxDepositAmountRequest { + /** + * Currency to deposit. + */ currency: string; + + /** + * Target bank account to deposit into. + */ depositPaytoUri?: string; + + /** + * Restrict the deposit to a certain scope. + */ + restrictScope?: ScopeInfo; } export const codecForGetMaxDepositAmountRequest = buildCodecForObject<GetMaxDepositAmountRequest>() .property("currency", codecForString()) .property("depositPaytoUri", codecOptional(codecForString())) + .property("restrictScope", codecOptional(codecForScopeInfo())) .build("GetAmountRequest"); export interface GetMaxPeerPushDebitAmountRequest { @@ -268,6 +281,13 @@ export const codecForGetMaxPeerPushDebitAmountRequest = export interface GetMaxDepositAmountResponse { effectiveAmount: AmountString; rawAmount: AmountString; + + /** + * Account restrictions that affect the max deposit amount. + */ + depositRestrictions?: { + [exchangeBaseUrl: string]: { [paytoUri: string]: AccountRestriction[] }; + }; } export interface GetMaxPeerPushDebitAmountResponse { diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts @@ -26,6 +26,7 @@ import { GlobalIDB } from "@gnu-taler/idb-bridge"; import { AbsoluteTime, + AccountRestriction, AgeRestriction, AllowedAuditorInfo, AllowedExchangeInfo, @@ -184,6 +185,8 @@ async function internalSelectPayCoins( "exchanges", "exchangeDetails", "coins", + "globalCurrencyAuditors", + "globalCurrencyExchanges", ] >, req: SelectPayCoinRequestNg, @@ -288,6 +291,8 @@ export async function selectPayCoinsInTx( "exchanges", "exchangeDetails", "coins", + "globalCurrencyAuditors", + "globalCurrencyExchanges", ] >, req: SelectPayCoinRequestNg, @@ -380,6 +385,8 @@ export async function selectPayCoins( "exchanges", "exchangeDetails", "coins", + "globalCurrencyAuditors", + "globalCurrencyExchanges", ], }, async (tx) => { @@ -756,7 +763,11 @@ export function findMatchingWire( wireMethod: string, depositPaytoUri: string | undefined, exchangeWireDetails: ExchangeWireDetails, -): { wireFee: AmountJson } | undefined { +): + | { ok: true; wireFee: AmountJson } + | { ok: false; accountRestrictions: Record<string, AccountRestriction[]> } + | undefined { + const accountRestrictions: Record<string, AccountRestriction[]> = {}; for (const acc of exchangeWireDetails.wireInfo.accounts) { const pp = parsePaytoUri(acc.payto_uri); checkLogicInvariant(!!pp); @@ -796,10 +807,18 @@ export function findMatchingWire( } return { + ok: true, wireFee: Amounts.parseOrThrow(wireFeeStr), }; } - return undefined; + if (Object.keys(accountRestrictions).length > 0) { + return { + ok: false, + accountRestrictions, + }; + } else { + return undefined; + } } function checkExchangeAccepted( @@ -831,6 +850,7 @@ interface SelectPayCandidatesRequest { currency: string; restrictWireMethod: string | undefined; depositPaytoUri?: string; + restrictScope?: ScopeInfo; restrictExchanges: ExchangeRestrictionSpec | undefined; requiredMinimumAge?: number; @@ -845,12 +865,20 @@ interface SelectPayCandidatesRequest { export interface PayCoinCandidates { coinAvailability: AvailableCoinsOfDenom[]; currentWireFeePerExchange: Record<string, AmountJson>; + depositRestrictions: Record<string, Record<string, AccountRestriction[]>>; } async function selectPayCandidates( wex: WalletExecutionContext, tx: WalletDbReadOnlyTransaction< - ["exchanges", "coinAvailability", "exchangeDetails", "denominations"] + [ + "exchanges", + "coinAvailability", + "exchangeDetails", + "denominations", + "globalCurrencyAuditors", + "globalCurrencyExchanges", + ] >, req: SelectPayCandidatesRequest, ): Promise<PayCoinCandidates> { @@ -861,26 +889,30 @@ async function selectPayCandidates( const denoms: AvailableCoinsOfDenom[] = []; const exchanges = await tx.exchanges.iter().toArray(); const wfPerExchange: Record<string, AmountJson> = {}; + const depositRestrictions: Record< + string, + Record<string, AccountRestriction[]> + > = {}; for (const exchange of exchanges) { const exchangeDetails = await getExchangeWireDetailsInTx( tx, exchange.baseUrl, ); - // 1. exchange has same currency + // Exchange has same currency if (exchangeDetails?.currency !== req.currency) { logger.shouldLogTrace() && logger.trace(`skipping ${exchange.baseUrl} due to currency mismatch`); continue; } - // 2. Exchange supports wire method (only for pay/deposit) + // Exchange supports wire method (only for pay/deposit) if (req.restrictWireMethod) { - const wire = findMatchingWire( + const wireMatch = findMatchingWire( req.restrictWireMethod, req.depositPaytoUri, exchangeDetails, ); - if (!wire) { + if (!wireMatch) { if (logger.shouldLogTrace()) { logger.trace( `skipping ${exchange.baseUrl} due to missing wire info mismatch`, @@ -888,10 +920,14 @@ async function selectPayCandidates( } continue; } - wfPerExchange[exchange.baseUrl] = wire.wireFee; + if (!wireMatch.ok) { + depositRestrictions[exchange.baseUrl] = wireMatch.accountRestrictions; + continue; + } + wfPerExchange[exchange.baseUrl] = wireMatch.wireFee; } - // 3. exchange is trusted in the exchange list or auditor list + // Exchange is trusted in the exchange list or auditor list let accepted = checkExchangeAccepted( exchangeDetails, req.restrictExchanges, @@ -903,7 +939,20 @@ async function selectPayCandidates( continue; } - // 4. filter coins restricted by age + const isInScope = req.restrictScope + ? await checkExchangeInScopeTx( + wex, + tx, + exchange.baseUrl, + req.restrictScope, + ) + : true; + + if (!isInScope) { + continue; + } + + // Filter coins restricted by age let ageLower = 0; let ageUpper = AgeRestriction.AGE_UNRESTRICTED; if (req.requiredMinimumAge) { @@ -926,7 +975,7 @@ async function selectPayCandidates( let numUsable = 0; - // 5. save denoms with how many coins are available + // Save denoms with how many coins are available // FIXME: Check that the individual denomination is audited! // FIXME: Should we exclude denominations that are // not spendable anymore? @@ -977,6 +1026,7 @@ async function selectPayCandidates( return { coinAvailability: denoms, currentWireFeePerExchange: wfPerExchange, + depositRestrictions: depositRestrictions, }; } @@ -1125,6 +1175,8 @@ async function internalSelectPeerCoins( "denominations", "refreshGroups", "exchangeDetails", + "globalCurrencyAuditors", + "globalCurrencyExchanges", ] >, req: PeerCoinSelectionRequest, @@ -1399,6 +1451,8 @@ export async function getMaxDepositAmount( "coinAvailability", "denominations", "exchangeDetails", + "globalCurrencyAuditors", + "globalCurrencyExchanges", ], }, async (tx): Promise<GetMaxDepositAmountResponse> => { @@ -1414,6 +1468,7 @@ export async function getMaxDepositAmount( currency: req.currency, restrictExchanges: undefined, restrictWireMethod, + restrictScope: req.restrictScope, depositPaytoUri: req.depositPaytoUri, requiredMinimumAge: undefined, includePendingCoins: true, diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -1473,22 +1473,8 @@ async function processDepositGroupPendingDeposit( if (!depositGroup.payCoinSelection) { logger.info("missing coin selection for deposit group, selecting now"); - const transitionDone = await wex.db.runReadWriteTx( - { - storeNames: [ - "contractTerms", - "exchanges", - "exchangeDetails", - "depositGroups", - "coins", - "coinAvailability", - "coinHistory", - "refreshGroups", - "refreshSessions", - "denominations", - "transactionsMeta", - ], - }, + const transitionDone = await wex.db.runAllStoresReadWriteTx( + {}, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -1442,89 +1442,72 @@ async function handleInsufficientFunds( // FIXME: Above code should go into the transaction. - await wex.db.runReadWriteTx( - { - storeNames: [ - "coinAvailability", - "coinHistory", - "coins", - "contractTerms", - "denominations", - "exchangeDetails", - "exchanges", - "purchases", - "refreshGroups", - "refreshSessions", - "transactionsMeta", - ], - }, - async (tx) => { - const p = await tx.purchases.get(proposalId); - if (!p) { - return; - } - const payInfo = p.payInfo; - if (!payInfo) { - return; - } - - const { contractData } = await expectProposalDownloadInTx( - wex, - tx, - proposal, - ); + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + const p = await tx.purchases.get(proposalId); + if (!p) { + return; + } + const payInfo = p.payInfo; + if (!payInfo) { + return; + } - for (let i = 0; i < payCoinSelection.coinPubs.length; i++) { - const coinPub = payCoinSelection.coinPubs[i]; - const contrib = payCoinSelection.coinContributions[i]; - prevPayCoins.push({ - coinPub, - contribution: Amounts.parseOrThrow(contrib), - }); - } + const { contractData } = await expectProposalDownloadInTx( + wex, + tx, + proposal, + ); - const res = await selectPayCoinsInTx(wex, tx, { - restrictExchanges: { - auditors: [], - exchanges: contractData.allowedExchanges, - }, - restrictWireMethod: contractData.wireMethod, - contractTermsAmount: Amounts.parseOrThrow(contractData.amount), - depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee), - prevPayCoins, - requiredMinimumAge: contractData.minimumAge, + for (let i = 0; i < payCoinSelection.coinPubs.length; i++) { + const coinPub = payCoinSelection.coinPubs[i]; + const contrib = payCoinSelection.coinContributions[i]; + prevPayCoins.push({ + coinPub, + contribution: Amounts.parseOrThrow(contrib), }); + } - switch (res.type) { - case "failure": - logger.trace("insufficient funds for coin re-selection"); - return; - case "prospective": - return; - case "success": - break; - default: - assertUnreachable(res); - } + const res = await selectPayCoinsInTx(wex, tx, { + restrictExchanges: { + auditors: [], + exchanges: contractData.allowedExchanges, + }, + restrictWireMethod: contractData.wireMethod, + contractTermsAmount: Amounts.parseOrThrow(contractData.amount), + depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee), + prevPayCoins, + requiredMinimumAge: contractData.minimumAge, + }); - // Convert to DB format - payInfo.payCoinSelection = { - coinContributions: res.coinSel.coins.map((x) => x.contribution), - coinPubs: res.coinSel.coins.map((x) => x.coinPub), - }; - payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); - await tx.purchases.put(p); - await ctx.updateTransactionMeta(tx); - await spendCoins(wex, tx, { - transactionId: ctx.transactionId, - coinPubs: payInfo.payCoinSelection.coinPubs, - contributions: payInfo.payCoinSelection.coinContributions.map((x) => - Amounts.parseOrThrow(x), - ), - refreshReason: RefreshReason.PayMerchant, - }); - }, - ); + switch (res.type) { + case "failure": + logger.trace("insufficient funds for coin re-selection"); + return; + case "prospective": + return; + case "success": + break; + default: + assertUnreachable(res); + } + + // Convert to DB format + payInfo.payCoinSelection = { + coinContributions: res.coinSel.coins.map((x) => x.contribution), + coinPubs: res.coinSel.coins.map((x) => x.coinPub), + }; + payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); + await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); + await spendCoins(wex, tx, { + transactionId: ctx.transactionId, + coinPubs: payInfo.payCoinSelection.coinPubs, + contributions: payInfo.payCoinSelection.coinContributions.map((x) => + Amounts.parseOrThrow(x), + ), + refreshReason: RefreshReason.PayMerchant, + }); + }); wex.ws.notify({ type: NotificationType.BalanceChange, @@ -2216,21 +2199,8 @@ export async function confirmPay( `recording payment on ${proposal.orderId} with session ID ${sessionId}`, ); - const transitionInfo = await wex.db.runReadWriteTx( - { - storeNames: [ - "coinAvailability", - "coinHistory", - "coins", - "denominations", - "exchangeDetails", - "exchanges", - "purchases", - "refreshGroups", - "refreshSessions", - "transactionsMeta", - ], - }, + const transitionInfo = await wex.db.runAllStoresReadWriteTx( + {}, async (tx) => { const p = await tx.purchases.get(proposal.proposalId); if (!p) {