diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
5 files changed, 29 insertions, 631 deletions
| diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index c6cd4732c..64217acab 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -76,9 +76,9 @@ import {    extractContractData,    generateDepositPermissions,    getTotalPaymentCost, -  selectPayCoinsNew,  } from "./pay-merchant.js";  import { getTotalRefreshCost } from "./refresh.js"; +import { selectPayCoinsNew } from "../util/coinSelection.js";  /**   * Logger. diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 8a98c8299..d9051b32f 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -63,6 +63,7 @@ import {    ExchangeRecord,    WalletStoresV1,  } from "../db.js"; +import { isWithdrawableDenom } from "../index.js";  import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";  import { checkDbInvariant } from "../util/invariants.js";  import { @@ -78,7 +79,6 @@ import {  } from "../util/retries.js";  import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";  import { runOperationWithErrorReporting } from "./common.js"; -import { isWithdrawableDenom } from "./withdraw.js";  const logger = new Logger("exchanges.ts"); diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index 25153f9fb..f8fa1d34d 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -24,12 +24,10 @@  /**   * Imports.   */ -import { GlobalIDB } from "@gnu-taler/idb-bridge";  import {    AbortingCoin,    AbortRequest,    AbsoluteTime, -  AgeRestriction,    AmountJson,    Amounts,    ApplyRefundResponse, @@ -44,9 +42,8 @@ import {    CoinStatus,    ConfirmPayResult,    ConfirmPayResultType, -  MerchantContractTerms, +  constructPayUri,    ContractTermsUtil, -  DenominationInfo,    Duration,    encodeCrock,    ForcedCoinSel, @@ -54,11 +51,13 @@ import {    HttpStatusCode,    j2s,    Logger, +  makeErrorDetail, +  makePendingOperationFailedError,    MerchantCoinRefundFailureStatus,    MerchantCoinRefundStatus,    MerchantCoinRefundSuccessStatus, +  MerchantContractTerms,    NotificationType, -  parsePaytoUri,    parsePayUri,    parseRefundUri,    PayCoinSelection, @@ -66,19 +65,24 @@ import {    PreparePayResultType,    PrepareRefundResult,    RefreshReason, -  strcmp, +  TalerError,    TalerErrorCode,    TalerErrorDetail,    TalerProtocolTimestamp, +  TalerProtocolViolationError,    TransactionType,    URL, -  constructPayUri, -  PayMerchantInsufficientBalanceDetails,  } from "@gnu-taler/taler-util"; +import { +  getHttpResponseErrorDetails, +  readSuccessResponseJsonOrErrorCode, +  readSuccessResponseJsonOrThrow, +  readTalerErrorResponse, +  readUnexpectedResponseDetails, +  throwUnexpectedRequestError, +} from "@gnu-taler/taler-util/http";  import { EddsaKeypair } from "../crypto/cryptoImplementation.js";  import { -  AllowedAuditorInfo, -  AllowedExchangeInfo,    BackupProviderStateTag,    CoinRecord,    DenominationRecord, @@ -89,51 +93,29 @@ import {    WalletContractData,    WalletStoresV1,  } from "../db.js"; -import { -  makeErrorDetail, -  makePendingOperationFailedError, -  TalerError, -  TalerProtocolViolationError, -} from "@gnu-taler/taler-util";  import { GetReadWriteAccess, PendingTaskType } from "../index.js";  import {    EXCHANGE_COINS_LOCK,    InternalWalletState,  } from "../internal-wallet-state.js";  import { assertUnreachable } from "../util/assertUnreachable.js"; +import { PreviousPayCoins, selectPayCoinsNew } from "../util/coinSelection.js"; +import { checkDbInvariant } from "../util/invariants.js"; +import { GetReadOnlyAccess } from "../util/query.js";  import { -  CoinSelectionTally, -  PreviousPayCoins, -  tallyFees, -} from "../util/coinSelection.js"; -import { -  getHttpResponseErrorDetails, -  readSuccessResponseJsonOrErrorCode, -  readSuccessResponseJsonOrThrow, -  readTalerErrorResponse, -  readUnexpectedResponseDetails, -  throwUnexpectedRequestError, -} from "@gnu-taler/taler-util/http"; -import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; -import { +  constructTaskIdentifier,    OperationAttemptResult,    OperationAttemptResultType,    RetryInfo, -  TaskIdentifiers,    scheduleRetry, -  constructTaskIdentifier, +  TaskIdentifiers,  } from "../util/retries.js";  import {    makeTransactionId,    runOperationWithErrorReporting,    spendCoins, -  storeOperationError, -  storeOperationPending,  } from "./common.js"; -import { getExchangeDetails } from "./exchanges.js";  import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js"; -import { GetReadOnlyAccess } from "../util/query.js"; -import { getMerchantPaymentBalanceDetails } from "./balance.js";  /**   * Logger. @@ -877,434 +859,6 @@ async function unblockBackup(      });  } -export interface SelectPayCoinRequestNg { -  exchanges: AllowedExchangeInfo[]; -  auditors: AllowedAuditorInfo[]; -  wireMethod: string; -  contractTermsAmount: AmountJson; -  depositFeeLimit: AmountJson; -  wireFeeLimit: AmountJson; -  wireFeeAmortization: number; -  prevPayCoins?: PreviousPayCoins; -  requiredMinimumAge?: number; -  forcedSelection?: ForcedCoinSel; -} - -export type AvailableDenom = DenominationInfo & { -  maxAge: number; -  numAvailable: number; -}; - -export async function selectCandidates( -  ws: InternalWalletState, -  req: SelectPayCoinRequestNg, -): Promise<[AvailableDenom[], Record<string, AmountJson>]> { -  return await ws.db -    .mktx((x) => [ -      x.exchanges, -      x.exchangeDetails, -      x.denominations, -      x.coinAvailability, -    ]) -    .runReadOnly(async (tx) => { -      // FIXME: Use the existing helper (from balance.ts) to -      // get acceptable exchanges. -      const denoms: AvailableDenom[] = []; -      const exchanges = await tx.exchanges.iter().toArray(); -      const wfPerExchange: Record<string, AmountJson> = {}; -      for (const exchange of exchanges) { -        const exchangeDetails = await getExchangeDetails(tx, exchange.baseUrl); -        if (exchangeDetails?.currency !== req.contractTermsAmount.currency) { -          continue; -        } -        let wireMethodSupported = false; -        for (const acc of exchangeDetails.wireInfo.accounts) { -          const pp = parsePaytoUri(acc.payto_uri); -          checkLogicInvariant(!!pp); -          if (pp.targetType === req.wireMethod) { -            wireMethodSupported = true; -            break; -          } -        } -        if (!wireMethodSupported) { -          break; -        } -        exchangeDetails.wireInfo.accounts; -        let accepted = false; -        for (const allowedExchange of req.exchanges) { -          if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) { -            accepted = true; -            break; -          } -        } -        for (const allowedAuditor of req.auditors) { -          for (const providedAuditor of exchangeDetails.auditors) { -            if (allowedAuditor.auditorPub === providedAuditor.auditor_pub) { -              accepted = true; -              break; -            } -          } -        } -        if (!accepted) { -          continue; -        } -        let ageLower = 0; -        let ageUpper = AgeRestriction.AGE_UNRESTRICTED; -        if (req.requiredMinimumAge) { -          ageLower = req.requiredMinimumAge; -        } -        const myExchangeDenoms = -          await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll( -            GlobalIDB.KeyRange.bound( -              [exchangeDetails.exchangeBaseUrl, ageLower, 1], -              [ -                exchangeDetails.exchangeBaseUrl, -                ageUpper, -                Number.MAX_SAFE_INTEGER, -              ], -            ), -          ); -        // FIXME: Check that the individual denomination is audited! -        // FIXME: Should we exclude denominations that are -        // not spendable anymore? -        for (const denomAvail of myExchangeDenoms) { -          const denom = await tx.denominations.get([ -            denomAvail.exchangeBaseUrl, -            denomAvail.denomPubHash, -          ]); -          checkDbInvariant(!!denom); -          if (denom.isRevoked || !denom.isOffered) { -            continue; -          } -          denoms.push({ -            ...DenominationRecord.toDenomInfo(denom), -            numAvailable: denomAvail.freshCoinCount ?? 0, -            maxAge: denomAvail.maxAge, -          }); -        } -      } -      // Sort by available amount (descending),  deposit fee (ascending) and -      // denomPub (ascending) if deposit fee is the same -      // (to guarantee deterministic results) -      denoms.sort( -        (o1, o2) => -          -Amounts.cmp(o1.value, o2.value) || -          Amounts.cmp(o1.feeDeposit, o2.feeDeposit) || -          strcmp(o1.denomPubHash, o2.denomPubHash), -      ); -      return [denoms, wfPerExchange]; -    }); -} - -function makeAvailabilityKey( -  exchangeBaseUrl: string, -  denomPubHash: string, -  maxAge: number, -): string { -  return `${denomPubHash};${maxAge};${exchangeBaseUrl}`; -} - -/** - * Selection result. - */ -interface SelResult { -  /** -   * Map from an availability key -   * to an array of contributions. -   */ -  [avKey: string]: { -    exchangeBaseUrl: string; -    denomPubHash: string; -    maxAge: number; -    contributions: AmountJson[]; -  }; -} - -export function selectGreedy( -  req: SelectPayCoinRequestNg, -  candidateDenoms: AvailableDenom[], -  wireFeesPerExchange: Record<string, AmountJson>, -  tally: CoinSelectionTally, -): SelResult | undefined { -  const { wireFeeAmortization } = req; -  const selectedDenom: SelResult = {}; -  for (const aci of candidateDenoms) { -    const contributions: AmountJson[] = []; -    for (let i = 0; i < aci.numAvailable; i++) { -      // Don't use this coin if depositing it is more expensive than -      // the amount it would give the merchant. -      if (Amounts.cmp(aci.feeDeposit, aci.value) > 0) { -        tally.lastDepositFee = Amounts.parseOrThrow(aci.feeDeposit); -        continue; -      } - -      if (Amounts.isZero(tally.amountPayRemaining)) { -        // We have spent enough! -        break; -      } - -      tally = tallyFees( -        tally, -        wireFeesPerExchange, -        wireFeeAmortization, -        aci.exchangeBaseUrl, -        Amounts.parseOrThrow(aci.feeDeposit), -      ); - -      let coinSpend = Amounts.max( -        Amounts.min(tally.amountPayRemaining, aci.value), -        aci.feeDeposit, -      ); - -      tally.amountPayRemaining = Amounts.sub( -        tally.amountPayRemaining, -        coinSpend, -      ).amount; -      contributions.push(coinSpend); -    } - -    if (contributions.length) { -      const avKey = makeAvailabilityKey( -        aci.exchangeBaseUrl, -        aci.denomPubHash, -        aci.maxAge, -      ); -      let sd = selectedDenom[avKey]; -      if (!sd) { -        sd = { -          contributions: [], -          denomPubHash: aci.denomPubHash, -          exchangeBaseUrl: aci.exchangeBaseUrl, -          maxAge: aci.maxAge, -        }; -      } -      sd.contributions.push(...contributions); -      selectedDenom[avKey] = sd; -    } - -    if (Amounts.isZero(tally.amountPayRemaining)) { -      return selectedDenom; -    } -  } -  return undefined; -} - -export function selectForced( -  req: SelectPayCoinRequestNg, -  candidateDenoms: AvailableDenom[], -): SelResult | undefined { -  const selectedDenom: SelResult = {}; - -  const forcedSelection = req.forcedSelection; -  checkLogicInvariant(!!forcedSelection); - -  for (const forcedCoin of forcedSelection.coins) { -    let found = false; -    for (const aci of candidateDenoms) { -      if (aci.numAvailable <= 0) { -        continue; -      } -      if (Amounts.cmp(aci.value, forcedCoin.value) === 0) { -        aci.numAvailable--; -        const avKey = makeAvailabilityKey( -          aci.exchangeBaseUrl, -          aci.denomPubHash, -          aci.maxAge, -        ); -        let sd = selectedDenom[avKey]; -        if (!sd) { -          sd = { -            contributions: [], -            denomPubHash: aci.denomPubHash, -            exchangeBaseUrl: aci.exchangeBaseUrl, -            maxAge: aci.maxAge, -          }; -        } -        sd.contributions.push(Amounts.parseOrThrow(forcedCoin.value)); -        selectedDenom[avKey] = sd; -        found = true; -        break; -      } -    } -    if (!found) { -      throw Error("can't find coin for forced coin selection"); -    } -  } - -  return selectedDenom; -} - -export type SelectPayCoinsResult = -  | { -      type: "failure"; -      insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails; -    } -  | { type: "success"; coinSel: PayCoinSelection }; - -/** - * Given a list of candidate coins, select coins to spend under the merchant's - * constraints. - * - * The prevPayCoins can be specified to "repair" a coin selection - * by adding additional coins, after a broken (e.g. double-spent) coin - * has been removed from the selection. - * - * This function is only exported for the sake of unit tests. - */ -export async function selectPayCoinsNew( -  ws: InternalWalletState, -  req: SelectPayCoinRequestNg, -): Promise<SelectPayCoinsResult> { -  const { -    contractTermsAmount, -    depositFeeLimit, -    wireFeeLimit, -    wireFeeAmortization, -  } = req; - -  const [candidateDenoms, wireFeesPerExchange] = await selectCandidates( -    ws, -    req, -  ); - -  // logger.trace(`candidate denoms: ${j2s(candidateDenoms)}`); - -  const coinPubs: string[] = []; -  const coinContributions: AmountJson[] = []; -  const currency = contractTermsAmount.currency; - -  let tally: CoinSelectionTally = { -    amountPayRemaining: contractTermsAmount, -    amountWireFeeLimitRemaining: wireFeeLimit, -    amountDepositFeeLimitRemaining: depositFeeLimit, -    customerDepositFees: Amounts.zeroOfCurrency(currency), -    customerWireFees: Amounts.zeroOfCurrency(currency), -    wireFeeCoveredForExchange: new Set(), -    lastDepositFee: Amounts.zeroOfCurrency(currency), -  }; - -  const prevPayCoins = req.prevPayCoins ?? []; - -  // Look at existing pay coin selection and tally up -  for (const prev of prevPayCoins) { -    tally = tallyFees( -      tally, -      wireFeesPerExchange, -      wireFeeAmortization, -      prev.exchangeBaseUrl, -      prev.feeDeposit, -    ); -    tally.amountPayRemaining = Amounts.sub( -      tally.amountPayRemaining, -      prev.contribution, -    ).amount; - -    coinPubs.push(prev.coinPub); -    coinContributions.push(prev.contribution); -  } - -  let selectedDenom: SelResult | undefined; -  if (req.forcedSelection) { -    selectedDenom = selectForced(req, candidateDenoms); -  } else { -    // FIXME:  Here, we should select coins in a smarter way. -    // Instead of always spending the next-largest coin, -    // we should try to find the smallest coin that covers the -    // amount. -    selectedDenom = selectGreedy( -      req, -      candidateDenoms, -      wireFeesPerExchange, -      tally, -    ); -  } - -  if (!selectedDenom) { -    const details = await getMerchantPaymentBalanceDetails(ws, { -      acceptedAuditors: req.auditors, -      acceptedExchanges: req.exchanges, -      acceptedWireMethods: [req.wireMethod], -      currency: Amounts.currencyOf(req.contractTermsAmount), -      minAge: req.requiredMinimumAge ?? 0, -    }); -    let feeGapEstimate: AmountJson; -    if ( -      Amounts.cmp( -        details.balanceMerchantDepositable, -        req.contractTermsAmount, -      ) >= 0 -    ) { -      // FIXME: We can probably give a better estimate. -      feeGapEstimate = Amounts.add( -        tally.amountPayRemaining, -        tally.lastDepositFee, -      ).amount; -    } else { -      feeGapEstimate = Amounts.zeroOfAmount(req.contractTermsAmount); -    } -    return { -      type: "failure", -      insufficientBalanceDetails: { -        amountRequested: Amounts.stringify(req.contractTermsAmount), -        balanceAgeAcceptable: Amounts.stringify(details.balanceAgeAcceptable), -        balanceAvailable: Amounts.stringify(details.balanceAvailable), -        balanceMaterial: Amounts.stringify(details.balanceMaterial), -        balanceMerchantAcceptable: Amounts.stringify( -          details.balanceMerchantAcceptable, -        ), -        balanceMerchantDepositable: Amounts.stringify( -          details.balanceMerchantDepositable, -        ), -        feeGapEstimate: Amounts.stringify(feeGapEstimate), -      }, -    }; -  } - -  const finalSel = selectedDenom; - -  logger.trace(`coin selection request ${j2s(req)}`); -  logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`); - -  await ws.db -    .mktx((x) => [x.coins, x.denominations]) -    .runReadOnly(async (tx) => { -      for (const dph of Object.keys(finalSel)) { -        const selInfo = finalSel[dph]; -        const numRequested = selInfo.contributions.length; -        const query = [ -          selInfo.exchangeBaseUrl, -          selInfo.denomPubHash, -          selInfo.maxAge, -          CoinStatus.Fresh, -        ]; -        logger.info(`query: ${j2s(query)}`); -        const coins = -          await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll( -            query, -            numRequested, -          ); -        if (coins.length != numRequested) { -          throw Error( -            `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`, -          ); -        } -        coinPubs.push(...coins.map((x) => x.coinPub)); -        coinContributions.push(...selInfo.contributions); -      } -    }); - -  return { -    type: "success", -    coinSel: { -      paymentAmount: Amounts.stringify(contractTermsAmount), -      coinContributions: coinContributions.map((x) => Amounts.stringify(x)), -      coinPubs, -      customerDepositFees: Amounts.stringify(tally.customerDepositFees), -      customerWireFees: Amounts.stringify(tally.customerWireFees), -    }, -  }; -} -  export async function checkPaymentByProposalId(    ws: InternalWalletState,    proposalId: string, @@ -1704,9 +1258,7 @@ export async function confirmPay(    const contractData = d.contractData; -  let selectCoinsResult: SelectPayCoinsResult | undefined = undefined; - -  selectCoinsResult = await selectPayCoinsNew(ws, { +  const selectCoinsResult = await selectPayCoinsNew(ws, {      auditors: contractData.allowedAuditors,      exchanges: contractData.allowedExchanges,      wireMethod: contractData.wireMethod, diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 477a00503..70f0579c0 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -85,10 +85,8 @@ import {  } from "../util/retries.js";  import { makeCoinAvailable } from "./common.js";  import { updateExchangeFromUrl } from "./exchanges.js"; -import { -  isWithdrawableDenom, -  selectWithdrawalDenominations, -} from "./withdraw.js"; +import { selectWithdrawalDenominations } from "../util/coinSelection.js"; +import { isWithdrawableDenom } from "../index.js";  const logger = new Logger("refresh.ts"); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 2c91d4184..643737e93 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -93,7 +93,6 @@ import {    runLongpollAsync,    runOperationWithErrorReporting,  } from "../operations/common.js"; -import { walletCoreDebugFlags } from "../util/debugFlags.js";  import {    HttpRequestLibrary,    HttpResponse, @@ -123,6 +122,11 @@ import {    getExchangeTrust,    updateExchangeFromUrl,  } from "./exchanges.js"; +import { +  selectForcedWithdrawalDenominations, +  selectWithdrawalDenominations, +} from "../util/coinSelection.js"; +import { isWithdrawableDenom } from "../index.js";  /**   * Logger for this file. @@ -130,162 +134,6 @@ import {  const logger = new Logger("operations/withdraw.ts");  /** - * Check if a denom is withdrawable based on the expiration time, - * revocation and offered state. - */ -export function isWithdrawableDenom(d: DenominationRecord): boolean { -  const now = AbsoluteTime.now(); -  const start = AbsoluteTime.fromTimestamp(d.stampStart); -  const withdrawExpire = AbsoluteTime.fromTimestamp(d.stampExpireWithdraw); -  const started = AbsoluteTime.cmp(now, start) >= 0; -  let lastPossibleWithdraw: AbsoluteTime; -  if (walletCoreDebugFlags.denomselAllowLate) { -    lastPossibleWithdraw = start; -  } else { -    lastPossibleWithdraw = AbsoluteTime.subtractDuraction( -      withdrawExpire, -      durationFromSpec({ minutes: 5 }), -    ); -  } -  const remaining = Duration.getRemaining(lastPossibleWithdraw, now); -  const stillOkay = remaining.d_ms !== 0; -  return started && stillOkay && !d.isRevoked && d.isOffered; -} - -/** - * Get a list of denominations (with repetitions possible) - * whose total value is as close as possible to the available - * amount, but never larger. - */ -export function selectWithdrawalDenominations( -  amountAvailable: AmountJson, -  denoms: DenominationRecord[], -): DenomSelectionState { -  let remaining = Amounts.copy(amountAvailable); - -  const selectedDenoms: { -    count: number; -    denomPubHash: string; -  }[] = []; - -  let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency); -  let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency); - -  denoms = denoms.filter(isWithdrawableDenom); -  denoms.sort((d1, d2) => -    Amounts.cmp( -      DenominationRecord.getValue(d2), -      DenominationRecord.getValue(d1), -    ), -  ); - -  for (const d of denoms) { -    let count = 0; -    const cost = Amounts.add( -      DenominationRecord.getValue(d), -      d.fees.feeWithdraw, -    ).amount; -    for (;;) { -      if (Amounts.cmp(remaining, cost) < 0) { -        break; -      } -      remaining = Amounts.sub(remaining, cost).amount; -      count++; -    } -    if (count > 0) { -      totalCoinValue = Amounts.add( -        totalCoinValue, -        Amounts.mult(DenominationRecord.getValue(d), count).amount, -      ).amount; -      totalWithdrawCost = Amounts.add( -        totalWithdrawCost, -        Amounts.mult(cost, count).amount, -      ).amount; -      selectedDenoms.push({ -        count, -        denomPubHash: d.denomPubHash, -      }); -    } - -    if (Amounts.isZero(remaining)) { -      break; -    } -  } - -  if (logger.shouldLogTrace()) { -    logger.trace( -      `selected withdrawal denoms for ${Amounts.stringify(totalCoinValue)}`, -    ); -    for (const sd of selectedDenoms) { -      logger.trace(`denom_pub_hash=${sd.denomPubHash}, count=${sd.count}`); -    } -    logger.trace("(end of withdrawal denom list)"); -  } - -  return { -    selectedDenoms, -    totalCoinValue: Amounts.stringify(totalCoinValue), -    totalWithdrawCost: Amounts.stringify(totalWithdrawCost), -  }; -} - -export function selectForcedWithdrawalDenominations( -  amountAvailable: AmountJson, -  denoms: DenominationRecord[], -  forcedDenomSel: ForcedDenomSel, -): DenomSelectionState { -  const selectedDenoms: { -    count: number; -    denomPubHash: string; -  }[] = []; - -  let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency); -  let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency); - -  denoms = denoms.filter(isWithdrawableDenom); -  denoms.sort((d1, d2) => -    Amounts.cmp( -      DenominationRecord.getValue(d2), -      DenominationRecord.getValue(d1), -    ), -  ); - -  for (const fds of forcedDenomSel.denoms) { -    const count = fds.count; -    const denom = denoms.find((x) => { -      return Amounts.cmp(DenominationRecord.getValue(x), fds.value) == 0; -    }); -    if (!denom) { -      throw Error( -        `unable to find denom for forced selection (value ${fds.value})`, -      ); -    } -    const cost = Amounts.add( -      DenominationRecord.getValue(denom), -      denom.fees.feeWithdraw, -    ).amount; -    totalCoinValue = Amounts.add( -      totalCoinValue, -      Amounts.mult(DenominationRecord.getValue(denom), count).amount, -    ).amount; -    totalWithdrawCost = Amounts.add( -      totalWithdrawCost, -      Amounts.mult(cost, count).amount, -    ).amount; -    selectedDenoms.push({ -      count, -      denomPubHash: denom.denomPubHash, -    }); -  } - -  return { -    selectedDenoms, -    totalCoinValue: Amounts.stringify(totalCoinValue), -    totalWithdrawCost: Amounts.stringify(totalWithdrawCost), -  }; -} - -/**   * Get information about a withdrawal from   * a taler://withdraw URI by asking the bank.   * | 
