towards new recoup API

This commit is contained in:
Florian Dold 2022-01-11 12:48:32 +01:00
parent fb22009ec4
commit a05e891d6e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
10 changed files with 317 additions and 172 deletions

View File

@ -479,6 +479,7 @@ export enum TalerSignaturePurpose {
MERCHANT_CONTRACT = 1101,
WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204,
WALLET_COIN_RECOUP_REFRESH = 1206,
EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
ANASTASIS_POLICY_UPLOAD = 1400,

View File

@ -46,7 +46,7 @@ import {
Duration,
codecForDuration,
} from "./time.js";
import { codecForAmountString } from "./amounts.js";
import { Amounts, codecForAmountString } from "./amounts.js";
/**
* Denomination as found in the /keys response from the exchange.
@ -149,9 +149,6 @@ export class ExchangeAuditor {
denomination_keys: AuditorDenomSig[];
}
/**
* Request that we send to the exchange to get a payback.
*/
export interface RecoupRequest {
/**
* Hashed enomination public key of the coin we want to get
@ -166,11 +163,6 @@ export interface RecoupRequest {
*/
denom_sig: UnblindedSignature | string;
/**
* Coin public key of the coin we want to refund.
*/
coin_pub: string;
/**
* Blinding key that was used during withdraw,
* used to prove that we were actually withdrawing the coin.
@ -178,14 +170,45 @@ export interface RecoupRequest {
coin_blind_key_secret: string;
/**
* Signature made by the coin, authorizing the payback.
* Signature of TALER_RecoupRequestPS created with the coin's private key.
*/
coin_sig: string;
/**
* Was the coin refreshed (and thus the recoup should go to the old coin)?
* Amount being recouped.
*/
refreshed: boolean;
amount: AmountString;
}
export interface RecoupRefreshRequest {
/**
* Hashed enomination public key of the coin we want to get
* paid back.
*/
denom_pub_hash: string;
/**
* Signature over the coin public key by the denomination.
*
* The string variant is for the legacy exchange protocol.
*/
denom_sig: UnblindedSignature | string;
/**
* Coin's blinding factor.
*/
coin_blind_key_secret: string;
/**
* Signature of TALER_RecoupRefreshRequestPS created with
* the coin's private key.
*/
coin_sig: string;
/**
* Amount being recouped.
*/
amount: AmountString;
}
/**
@ -1131,7 +1154,8 @@ export const codecForLegacyRsaDenominationPubKey = () =>
.property("rsa_public_key", codecForString())
.build("LegacyRsaDenominationPubKey");
export const codecForBankWithdrawalOperationPostResponse = (): Codec<BankWithdrawalOperationPostResponse> =>
export const codecForBankWithdrawalOperationPostResponse =
(): Codec<BankWithdrawalOperationPostResponse> =>
buildCodecForObject<BankWithdrawalOperationPostResponse>()
.property("transfer_done", codecForBoolean())
.build("BankWithdrawalOperationPostResponse");
@ -1213,8 +1237,8 @@ export const codecForTax = (): Codec<Tax> =>
.property("tax", codecForString())
.build("Tax");
export const codecForInternationalizedString = (): Codec<InternationalizedString> =>
codecForMap(codecForString());
export const codecForInternationalizedString =
(): Codec<InternationalizedString> => codecForMap(codecForString());
export const codecForProduct = (): Codec<Product> =>
buildCodecForObject<Product>()
@ -1262,7 +1286,8 @@ export const codecForContractTerms = (): Codec<ContractTerms> =>
.property("extra", codecForAny())
.build("ContractTerms");
export const codecForMerchantRefundPermission = (): Codec<MerchantAbortPayRefundDetails> =>
export const codecForMerchantRefundPermission =
(): Codec<MerchantAbortPayRefundDetails> =>
buildCodecForObject<MerchantAbortPayRefundDetails>()
.property("refund_amount", codecForAmountString())
.property("refund_fee", codecForAmountString())
@ -1275,14 +1300,16 @@ export const codecForMerchantRefundPermission = (): Codec<MerchantAbortPayRefund
.property("exchange_pub", codecOptional(codecForString()))
.build("MerchantRefundPermission");
export const codecForMerchantRefundResponse = (): Codec<MerchantRefundResponse> =>
export const codecForMerchantRefundResponse =
(): Codec<MerchantRefundResponse> =>
buildCodecForObject<MerchantRefundResponse>()
.property("merchant_pub", codecForString())
.property("h_contract_terms", codecForString())
.property("refunds", codecForList(codecForMerchantRefundPermission()))
.build("MerchantRefundResponse");
export const codecForMerchantBlindSigWrapperV1 = (): Codec<MerchantBlindSigWrapperV1> =>
export const codecForMerchantBlindSigWrapperV1 =
(): Codec<MerchantBlindSigWrapperV1> =>
buildCodecForObject<MerchantBlindSigWrapperV1>()
.property("blind_sig", codecForString())
.build("BlindSigWrapper");
@ -1365,7 +1392,8 @@ export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
.property("contract_url", codecOptional(codecForString()))
.build("CheckPaymentResponse");
export const codecForWithdrawOperationStatusResponse = (): Codec<WithdrawOperationStatusResponse> =>
export const codecForWithdrawOperationStatusResponse =
(): Codec<WithdrawOperationStatusResponse> =>
buildCodecForObject<WithdrawOperationStatusResponse>()
.property("selection_done", codecForBoolean())
.property("transfer_done", codecForBoolean())
@ -1419,12 +1447,14 @@ export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
)
.build("ExchangeRevealItem");
export const codecForExchangeRevealResponse = (): Codec<ExchangeRevealResponse> =>
export const codecForExchangeRevealResponse =
(): Codec<ExchangeRevealResponse> =>
buildCodecForObject<ExchangeRevealResponse>()
.property("ev_sigs", codecForList(codecForExchangeRevealItem()))
.build("ExchangeRevealResponse");
export const codecForMerchantCoinRefundSuccessStatus = (): Codec<MerchantCoinRefundSuccessStatus> =>
export const codecForMerchantCoinRefundSuccessStatus =
(): Codec<MerchantCoinRefundSuccessStatus> =>
buildCodecForObject<MerchantCoinRefundSuccessStatus>()
.property("type", codecForConstString("success"))
.property("coin_pub", codecForString())
@ -1436,7 +1466,8 @@ export const codecForMerchantCoinRefundSuccessStatus = (): Codec<MerchantCoinRef
.property("execution_time", codecForTimestamp)
.build("MerchantCoinRefundSuccessStatus");
export const codecForMerchantCoinRefundFailureStatus = (): Codec<MerchantCoinRefundFailureStatus> =>
export const codecForMerchantCoinRefundFailureStatus =
(): Codec<MerchantCoinRefundFailureStatus> =>
buildCodecForObject<MerchantCoinRefundFailureStatus>()
.property("type", codecForConstString("failure"))
.property("coin_pub", codecForString())
@ -1448,27 +1479,31 @@ export const codecForMerchantCoinRefundFailureStatus = (): Codec<MerchantCoinRef
.property("execution_time", codecForTimestamp)
.build("MerchantCoinRefundFailureStatus");
export const codecForMerchantCoinRefundStatus = (): Codec<MerchantCoinRefundStatus> =>
export const codecForMerchantCoinRefundStatus =
(): Codec<MerchantCoinRefundStatus> =>
buildCodecForUnion<MerchantCoinRefundStatus>()
.discriminateOn("type")
.alternative("success", codecForMerchantCoinRefundSuccessStatus())
.alternative("failure", codecForMerchantCoinRefundFailureStatus())
.build("MerchantCoinRefundStatus");
export const codecForMerchantOrderStatusPaid = (): Codec<MerchantOrderStatusPaid> =>
export const codecForMerchantOrderStatusPaid =
(): Codec<MerchantOrderStatusPaid> =>
buildCodecForObject<MerchantOrderStatusPaid>()
.property("refund_amount", codecForString())
.property("refunded", codecForBoolean())
.build("MerchantOrderStatusPaid");
export const codecForMerchantOrderRefundPickupResponse = (): Codec<MerchantOrderRefundResponse> =>
export const codecForMerchantOrderRefundPickupResponse =
(): Codec<MerchantOrderRefundResponse> =>
buildCodecForObject<MerchantOrderRefundResponse>()
.property("merchant_pub", codecForString())
.property("refund_amount", codecForString())
.property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
.build("MerchantOrderRefundPickupResponse");
export const codecForMerchantOrderStatusUnpaid = (): Codec<MerchantOrderStatusUnpaid> =>
export const codecForMerchantOrderStatusUnpaid =
(): Codec<MerchantOrderStatusUnpaid> =>
buildCodecForObject<MerchantOrderStatusUnpaid>()
.property("taler_pay_uri", codecForString())
.property("already_paid_order_id", codecOptional(codecForString()))
@ -1550,7 +1585,8 @@ export interface MerchantAbortPayRefundSuccessStatus {
exchange_pub: string;
}
export const codecForMerchantAbortPayRefundSuccessStatus = (): Codec<MerchantAbortPayRefundSuccessStatus> =>
export const codecForMerchantAbortPayRefundSuccessStatus =
(): Codec<MerchantAbortPayRefundSuccessStatus> =>
buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
@ -1558,7 +1594,8 @@ export const codecForMerchantAbortPayRefundSuccessStatus = (): Codec<MerchantAbo
.property("type", codecForConstString("success"))
.build("MerchantAbortPayRefundSuccessStatus");
export const codecForMerchantAbortPayRefundFailureStatus = (): Codec<MerchantAbortPayRefundFailureStatus> =>
export const codecForMerchantAbortPayRefundFailureStatus =
(): Codec<MerchantAbortPayRefundFailureStatus> =>
buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
.property("exchange_code", codecForNumber())
.property("exchange_reply", codecForAny())
@ -1566,7 +1603,8 @@ export const codecForMerchantAbortPayRefundFailureStatus = (): Codec<MerchantAbo
.property("type", codecForConstString("failure"))
.build("MerchantAbortPayRefundFailureStatus");
export const codecForMerchantAbortPayRefundStatus = (): Codec<MerchantAbortPayRefundStatus> =>
export const codecForMerchantAbortPayRefundStatus =
(): Codec<MerchantAbortPayRefundStatus> =>
buildCodecForUnion<MerchantAbortPayRefundStatus>()
.discriminateOn("type")
.alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
@ -1614,14 +1652,14 @@ export interface MerchantConfigResponse {
version: string;
}
export const codecForMerchantConfigResponse = (): Codec<MerchantConfigResponse> =>
export const codecForMerchantConfigResponse =
(): Codec<MerchantConfigResponse> =>
buildCodecForObject<MerchantConfigResponse>()
.property("currency", codecForString())
.property("name", codecForString())
.property("version", codecForString())
.build("MerchantConfigResponse");
export enum ExchangeProtocolVersion {
V9 = 9,
V12 = 12,

View File

@ -2031,9 +2031,9 @@ export class WalletCli {
`wallet-${self.name}`,
`taler-wallet-cli ${
self.timetravelArg ?? ""
} --no-throttle --wallet-db '${self.dbfile}' api '${op}' ${shellWrap(
JSON.stringify(payload),
)}`,
} --no-throttle -LTRACE --wallet-db '${
self.dbfile
}' api '${op}' ${shellWrap(JSON.stringify(payload))}`,
);
console.log("--- wallet core response ---");
console.log(resp);
@ -2080,6 +2080,7 @@ export class WalletCli {
[
"--no-throttle",
...this.timetravelArgArr,
"-LTRACE",
"--wallet-db",
this.dbfile,
"run-until-done",
@ -2095,6 +2096,7 @@ export class WalletCli {
"taler-wallet-cli",
[
"--no-throttle",
"-LTRACE",
...this.timetravelArgArr,
"--wallet-db",
this.dbfile,

View File

@ -249,6 +249,7 @@ walletCli
.action(async (args) => {
await withWallet(args, async (wallet) => {
let requestJson;
logger.info(`handling 'api' request (${args.api.operation})`);
try {
requestJson = JSON.parse(args.api.request);
} catch (e) {
@ -293,12 +294,6 @@ walletCli
});
});
async function asyncSleep(milliSeconds: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
setTimeout(() => resolve(), milliSeconds);
});
}
walletCli
.subcommand("runPendingOpt", "run-pending", {
help: "Run pending operations.",
@ -330,6 +325,7 @@ walletCli
.maybeOption("maxRetries", ["--max-retries"], clk.INT)
.action(async (args) => {
await withWallet(args, async (wallet) => {
logger.info("running until pending operations are finished");
await wallet.ws.runTaskLoop({
maxRetries: args.finishPendingOpt.maxRetries,
stopWhenDone: true,

View File

@ -27,7 +27,13 @@
/**
* Imports.
*/
import { AmountJson, DenominationPubKey, ExchangeProtocolVersion } from "@gnu-taler/taler-util";
import {
AmountJson,
AmountString,
DenominationPubKey,
ExchangeProtocolVersion,
UnblindedSignature,
} from "@gnu-taler/taler-util";
export interface RefreshNewDenomInfo {
count: number;
@ -140,3 +146,29 @@ export interface SignTrackTransactionRequest {
merchantPriv: string;
merchantPub: string;
}
/**
* Request to create a recoup request payload.
*/
export interface CreateRecoupReqRequest {
coinPub: string;
coinPriv: string;
blindingKey: string;
denomPub: DenominationPubKey;
denomPubHash: string;
denomSig: UnblindedSignature;
recoupAmount: AmountJson;
}
/**
* Request to create a recoup-refresh request payload.
*/
export interface CreateRecoupRefreshReqRequest {
coinPub: string;
coinPriv: string;
blindingKey: string;
denomPub: DenominationPubKey;
denomPubHash: string;
denomSig: UnblindedSignature;
recoupAmount: AmountJson;
}

View File

@ -26,7 +26,11 @@ import { CoinRecord, DenominationRecord, WireFee } from "../../db.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
import { RecoupRequest, CoinDepositPermission } from "@gnu-taler/taler-util";
import {
CoinDepositPermission,
RecoupRefreshRequest,
RecoupRequest,
} from "@gnu-taler/taler-util";
import {
BenchmarkResult,
@ -39,6 +43,8 @@ import {
import * as timer from "../../util/timer.js";
import { Logger } from "@gnu-taler/taler-util";
import {
CreateRecoupRefreshReqRequest,
CreateRecoupReqRequest,
DerivedRefreshSession,
DerivedTipPlanchet,
DeriveRefreshSessionRequest,
@ -421,8 +427,18 @@ export class CryptoApi {
);
}
createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> {
return this.doRpc<RecoupRequest>("createRecoupRequest", 1, coin);
createRecoupRequest(req: CreateRecoupReqRequest): Promise<RecoupRequest> {
return this.doRpc<RecoupRequest>("createRecoupRequest", 1, req);
}
createRecoupRefreshRequest(
req: CreateRecoupRefreshReqRequest,
): Promise<RecoupRefreshRequest> {
return this.doRpc<RecoupRefreshRequest>(
"createRecoupRefreshRequest",
1,
req,
);
}
deriveRefreshSession(

View File

@ -25,12 +25,7 @@
*/
// FIXME: Crypto should not use DB Types!
import {
CoinRecord,
DenominationRecord,
WireFee,
CoinSourceType,
} from "../../db.js";
import { DenominationRecord, WireFee } from "../../db.js";
import {
buildSigPS,
@ -39,6 +34,7 @@ import {
ExchangeProtocolVersion,
FreshCoin,
hashDenomPub,
RecoupRefreshRequest,
RecoupRequest,
RefreshPlanchetInfo,
TalerSignaturePurpose,
@ -78,6 +74,8 @@ import { Timestamp, timestampTruncateToSecond } from "@gnu-taler/taler-util";
import { Logger } from "@gnu-taler/taler-util";
import {
CreateRecoupRefreshReqRequest,
CreateRecoupReqRequest,
DerivedRefreshSession,
DerivedTipPlanchet,
DeriveRefreshSessionRequest,
@ -261,33 +259,64 @@ export class CryptoImplementation {
/**
* Create and sign a message to recoup a coin.
*/
createRecoupRequest(coin: CoinRecord): RecoupRequest {
createRecoupRequest(req: CreateRecoupReqRequest): RecoupRequest {
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP)
.put(decodeCrock(coin.coinPub))
.put(decodeCrock(coin.denomPubHash))
.put(decodeCrock(coin.blindingKey))
.put(decodeCrock(req.denomPubHash))
.put(decodeCrock(req.blindingKey))
.put(amountToBuffer(Amounts.jsonifyAmount(req.recoupAmount)))
.build();
const coinPriv = decodeCrock(coin.coinPriv);
const coinPriv = decodeCrock(req.coinPriv);
const coinSig = eddsaSign(p, coinPriv);
if (coin.denomPub.cipher === DenomKeyType.LegacyRsa) {
if (req.denomPub.cipher === DenomKeyType.LegacyRsa) {
const paybackRequest: RecoupRequest = {
coin_blind_key_secret: coin.blindingKey,
coin_pub: coin.coinPub,
coin_blind_key_secret: req.blindingKey,
coin_sig: encodeCrock(coinSig),
denom_pub_hash: coin.denomPubHash,
denom_sig: coin.denomSig.rsa_signature,
refreshed: coin.coinSource.type === CoinSourceType.Refresh,
denom_pub_hash: req.denomPubHash,
denom_sig: req.denomSig.rsa_signature,
amount: Amounts.stringify(req.recoupAmount),
};
return paybackRequest;
} else {
const paybackRequest: RecoupRequest = {
coin_blind_key_secret: coin.blindingKey,
coin_pub: coin.coinPub,
coin_blind_key_secret: req.blindingKey,
coin_sig: encodeCrock(coinSig),
denom_pub_hash: coin.denomPubHash,
denom_sig: coin.denomSig,
refreshed: coin.coinSource.type === CoinSourceType.Refresh,
denom_pub_hash: req.denomPubHash,
denom_sig: req.denomSig,
amount: Amounts.stringify(req.recoupAmount),
};
return paybackRequest;
}
}
/**
* Create and sign a message to recoup a coin.
*/
createRecoupRefreshRequest(req: CreateRecoupRefreshReqRequest): RecoupRefreshRequest {
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH)
.put(decodeCrock(req.denomPubHash))
.put(decodeCrock(req.blindingKey))
.put(amountToBuffer(Amounts.jsonifyAmount(req.recoupAmount)))
.build();
const coinPriv = decodeCrock(req.coinPriv);
const coinSig = eddsaSign(p, coinPriv);
if (req.denomPub.cipher === DenomKeyType.LegacyRsa) {
const paybackRequest: RecoupRefreshRequest = {
coin_blind_key_secret: req.blindingKey,
coin_sig: encodeCrock(coinSig),
denom_pub_hash: req.denomPubHash,
denom_sig: req.denomSig.rsa_signature,
amount: Amounts.stringify(req.recoupAmount),
};
return paybackRequest;
} else {
const paybackRequest: RecoupRefreshRequest = {
coin_blind_key_secret: req.blindingKey,
coin_sig: encodeCrock(coinSig),
denom_pub_hash: req.denomPubHash,
denom_sig: req.denomSig,
amount: Amounts.stringify(req.recoupAmount),
};
return paybackRequest;
}

View File

@ -651,7 +651,7 @@ async function updateExchangeFromUrlImpl(
logger.trace("denom already revoked");
continue;
}
logger.trace("revoking denom", recoupInfo.h_denom_pub);
logger.info("revoking denom", recoupInfo.h_denom_pub);
oldDenom.isRevoked = true;
await tx.denominations.put(oldDenom);
const affectedCoins = await tx.coins.indexes.byDenomPubHash
@ -662,7 +662,7 @@ async function updateExchangeFromUrlImpl(
}
}
if (newlyRevokedCoinPubs.length != 0) {
logger.trace("recouping coins", newlyRevokedCoinPubs);
logger.info("recouping coins", newlyRevokedCoinPubs);
recoupGroupId = await ws.recoupOps.createRecoupGroup(
ws,
tx,

View File

@ -28,6 +28,7 @@ import {
Amounts,
codecForRecoupConfirmation,
getTimestampNow,
j2s,
NotificationType,
RefreshReason,
TalerErrorDetails,
@ -107,7 +108,7 @@ async function putGroupAsFinished(
}
}
if (allFinished) {
logger.trace("all recoups of recoup group are finished");
logger.info("all recoups of recoup group are finished");
recoupGroup.timestampFinished = getTimestampNow();
recoupGroup.retryInfo = initRetryInfo();
recoupGroup.lastError = undefined;
@ -178,8 +179,17 @@ async function recoupWithdrawCoin(
type: NotificationType.RecoupStarted,
});
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
const recoupRequest = await ws.cryptoApi.createRecoupRequest({
blindingKey: coin.blindingKey,
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
denomPub: coin.denomPub,
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
recoupAmount: coin.currentAmount,
});
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
logger.trace(`requesting recoup via ${reqUrl.href}`);
const resp = await ws.http.postJson(reqUrl.href, recoupRequest, {
timeout: getReserveRequestTimeout(reserve),
});
@ -188,6 +198,8 @@ async function recoupWithdrawCoin(
codecForRecoupConfirmation(),
);
logger.trace(`got recoup confirmation ${j2s(recoupConfirmation)}`);
if (recoupConfirmation.reserve_pub !== reservePub) {
throw Error(`Coin's reserve doesn't match reserve on recoup`);
}
@ -249,7 +261,15 @@ async function recoupRefreshCoin(
type: NotificationType.RecoupStarted,
});
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
const recoupRequest = await ws.cryptoApi.createRecoupRefreshRequest({
blindingKey: coin.blindingKey,
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
denomPub: coin.denomPub,
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
recoupAmount: coin.currentAmount,
});
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
logger.trace(`making recoup request for ${coin.coinPub}`);
@ -359,9 +379,14 @@ async function processRecoupGroupImpl(
logger.trace("recoup group finished");
return;
}
const ps = recoupGroup.coinPubs.map((x, i) =>
processRecoup(ws, recoupGroupId, i),
);
const ps = recoupGroup.coinPubs.map(async (x, i) => {
try {
processRecoup(ws, recoupGroupId, i);
} catch (e) {
logger.warn(`processRecoup failed: ${e}`);
throw e;
}
});
await Promise.all(ps);
const reserveSet = new Set<string>();

View File

@ -30,6 +30,7 @@ import {
encodeCrock,
getRandomBytes,
getTimestampNow,
j2s,
Logger,
NotificationType,
randomBytes,
@ -538,6 +539,7 @@ async function updateReserve(
resp,
codecForReserveStatus(),
);
if (result.isError) {
if (
resp.status === 404 &&
@ -555,6 +557,8 @@ async function updateReserve(
}
}
logger.trace(`got reserve status ${j2s(result.response)}`);
const reserveInfo = result.response;
const balance = Amounts.parseOrThrow(reserveInfo.balance);
const currency = balance.currency;
@ -635,8 +639,10 @@ async function updateReserve(
}
}
const remainingAmount = Amounts.sub(amountReservePlus, amountReserveMinus)
.amount;
const remainingAmount = Amounts.sub(
amountReservePlus,
amountReserveMinus,
).amount;
const denomSelInfo = selectWithdrawalDenominations(
remainingAmount,
denoms,