wallet-core: do not rely on reserve history for withdrawals

This commit is contained in:
Florian Dold 2022-03-10 16:30:24 +01:00
parent 1607c728bc
commit 9d66078852
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
11 changed files with 194 additions and 130 deletions

View File

@ -43,15 +43,9 @@ export interface ReserveStatus {
* Balance left in the reserve.
*/
balance: AmountString;
/**
* Transaction history for the reserve.
*/
history: ReserveTransaction[];
}
export const codecForReserveStatus = (): Codec<ReserveStatus> =>
buildCodecForObject<ReserveStatus>()
.property("balance", codecForString())
.property("history", codecForList(codecForReserveTransaction()))
.build("ReserveStatus");

View File

@ -597,9 +597,6 @@ export interface PlanchetRecord {
denomPubHash: string;
// FIXME: maybe too redundant?
denomPub: DenominationPubKey;
blindingKey: string;
withdrawSig: string;
@ -607,10 +604,6 @@ export interface PlanchetRecord {
coinEv: CoinEnvelope;
coinEvHash: string;
coinValue: AmountJson;
isFromTip: boolean;
}
/**
@ -685,11 +678,6 @@ export interface CoinRecord {
*/
coinPriv: string;
/**
* Key used by the exchange used to sign the coin.
*/
denomPub: DenominationPubKey;
/**
* Hash of the public key that signs the coin.
*/
@ -1378,6 +1366,8 @@ export interface WithdrawalGroupRecord {
/**
* When was the withdrawal operation completed?
*
* FIXME: We should probably drop this and introduce an OperationStatus field.
*/
timestampFinish?: Timestamp;

View File

@ -419,7 +419,6 @@ export async function importBackup(
coinPub: compCoin.coinPub,
suspended: false,
exchangeBaseUrl: backupExchangeDetails.base_url,
denomPub: backupDenomination.denom_pub,
denomPubHash,
status: backupCoin.fresh
? CoinStatus.Fresh

View File

@ -315,7 +315,7 @@ export async function getCandidatePayCoins(
candidateCoins.push({
availableAmount: coin.currentAmount,
coinPub: coin.coinPub,
denomPub: coin.denomPub,
denomPub: denom.denomPub,
feeDeposit: denom.feeDeposit,
exchangeBaseUrl: denom.exchangeBaseUrl,
});
@ -1397,7 +1397,7 @@ export async function generateDepositPermissions(
coinPub: coin.coinPub,
contractTermsHash: contractData.contractTermsHash,
denomPubHash: coin.denomPubHash,
denomKeyType: coin.denomPub.cipher,
denomKeyType: denom.denomPub.cipher,
denomSig: coin.denomSig,
exchangeBaseUrl: coin.exchangeBaseUrl,
feeDeposit: denom.feeDeposit,

View File

@ -164,18 +164,34 @@ async function recoupWithdrawCoin(
cs: WithdrawCoinSource,
): Promise<void> {
const reservePub = cs.reservePub;
const reserve = await ws.db
const d = await ws.db
.mktx((x) => ({
reserves: x.reserves,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
return tx.reserves.get(reservePub);
const reserve = await tx.reserves.get(reservePub);
if (!reserve) {
return;
}
const denomInfo = await ws.getDenomInfo(
ws,
tx,
reserve.exchangeBaseUrl,
coin.denomPubHash,
);
if (!denomInfo) {
return;
}
return { reserve, denomInfo };
});
if (!reserve) {
if (!d) {
// FIXME: We should at least emit some pending operation / warning for this?
return;
}
const { reserve, denomInfo } = d;
ws.notify({
type: NotificationType.RecoupStarted,
});
@ -184,7 +200,7 @@ async function recoupWithdrawCoin(
blindingKey: coin.blindingKey,
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
denomPub: coin.denomPub,
denomPub: denomInfo.denomPub,
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
});
@ -253,6 +269,28 @@ async function recoupRefreshCoin(
coin: CoinRecord,
cs: RefreshCoinSource,
): Promise<void> {
const d = await ws.db
.mktx((x) => ({
coins: x.coins,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
const denomInfo = await ws.getDenomInfo(
ws,
tx,
coin.exchangeBaseUrl,
coin.denomPubHash,
);
if (!denomInfo) {
return;
}
return { denomInfo };
});
if (!d) {
// FIXME: We should at least emit some pending operation / warning for this?
return;
}
ws.notify({
type: NotificationType.RecoupStarted,
});
@ -261,7 +299,7 @@ async function recoupRefreshCoin(
blindingKey: coin.blindingKey,
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
denomPub: coin.denomPub,
denomPub: d.denomInfo.denomPub,
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
});

View File

@ -395,7 +395,7 @@ async function refreshMelt(
oldCoin.exchangeBaseUrl,
);
let meltReqBody: any;
if (oldCoin.denomPub.cipher === DenomKeyType.Rsa) {
if (oldDenom.denomPub.cipher === DenomKeyType.Rsa) {
meltReqBody = {
coin_pub: oldCoin.coinPub,
confirm_sig: derived.confirmSig,
@ -671,7 +671,6 @@ async function refreshReveal(
coinPriv: pc.coinPriv,
coinPub: pc.coinPub,
currentAmount: denom.value,
denomPub: denom.denomPub,
denomPubHash: denom.denomPubHash,
denomSig: {
cipher: DenomKeyType.Rsa,

View File

@ -587,8 +587,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;
const reserveBalance = Amounts.parseOrThrow(reserveInfo.balance);
const currency = reserveBalance.currency;
await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
const denoms = await getCandidateWithdrawalDenoms(
@ -598,73 +598,50 @@ async function updateReserve(
const newWithdrawalGroup = await ws.db
.mktx((x) => ({
coins: x.coins,
planchets: x.planchets,
withdrawalGroups: x.withdrawalGroups,
reserves: x.reserves,
denominations: x.denominations,
}))
.runReadWrite(async (tx) => {
const newReserve = await tx.reserves.get(reserve.reservePub);
if (!newReserve) {
return;
}
let amountReservePlus = Amounts.getZero(currency);
let amountReservePlus = reserveBalance;
let amountReserveMinus = Amounts.getZero(currency);
// Subtract withdrawal groups for this reserve from the available amount.
// Subtract amount allocated in unfinished withdrawal groups
// for this reserve from the available amount.
await tx.withdrawalGroups.indexes.byReservePub
.iter(reservePub)
.forEach((wg) => {
const cost = wg.denomsSel.totalWithdrawCost;
amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount;
});
for (const entry of reserveInfo.history) {
switch (entry.type) {
case ReserveTransactionType.Credit:
amountReservePlus = Amounts.add(
amountReservePlus,
Amounts.parseOrThrow(entry.amount),
).amount;
break;
case ReserveTransactionType.Recoup:
amountReservePlus = Amounts.add(
amountReservePlus,
Amounts.parseOrThrow(entry.amount),
).amount;
break;
case ReserveTransactionType.Closing:
amountReserveMinus = Amounts.add(
amountReserveMinus,
Amounts.parseOrThrow(entry.amount),
).amount;
break;
case ReserveTransactionType.Withdraw: {
// Now we check if the withdrawal transaction
// is part of any withdrawal known to this wallet.
const planchet = await tx.planchets.indexes.byCoinEvHash.get(
entry.h_coin_envelope,
);
if (planchet) {
// Amount is already accounted in some withdrawal session
break;
}
const coin = await tx.coins.indexes.byCoinEvHash.get(
entry.h_coin_envelope,
);
if (coin) {
// Amount is already accounted in some withdrawal session
break;
}
// Amount has been claimed by some withdrawal we don't know about
amountReserveMinus = Amounts.add(
amountReserveMinus,
Amounts.parseOrThrow(entry.amount),
).amount;
break;
.forEachAsync(async (wg) => {
if (wg.timestampFinish) {
return;
}
}
}
await tx.planchets.indexes.byGroup
.iter(wg.withdrawalGroupId)
.forEachAsync(async (pr) => {
if (pr.withdrawalDone) {
return;
}
const denomInfo = await ws.getDenomInfo(
ws,
tx,
wg.exchangeBaseUrl,
pr.denomPubHash,
);
if (!denomInfo) {
logger.error(`no denom info found for ${pr.denomPubHash}`);
return;
}
amountReserveMinus = Amounts.add(
amountReserveMinus,
denomInfo.value,
denomInfo.feeWithdraw,
).amount;
});
});
const remainingAmount = Amounts.sub(
amountReservePlus,

View File

@ -374,7 +374,6 @@ async function processTipImpl(
walletTipId: walletTipId,
},
currentAmount: denom.value,
denomPub: denom.denomPub,
denomPubHash: denom.denomPubHash,
denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa },
exchangeBaseUrl: tipRecord.exchangeBaseUrl,

View File

@ -418,10 +418,7 @@ async function processPlanchetGenerate(
coinIdx,
coinPriv: r.coinPriv,
coinPub: r.coinPub,
coinValue: r.coinValue,
denomPub: r.denomPub,
denomPubHash: r.denomPubHash,
isFromTip: false,
reservePub: r.reservePub,
withdrawalDone: false,
withdrawSig: r.withdrawSig,
@ -557,6 +554,7 @@ async function processPlanchetVerifyAndStoreCoin(
.mktx((x) => ({
withdrawalGroups: x.withdrawalGroups,
planchets: x.planchets,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
@ -570,16 +568,29 @@ async function processPlanchetVerifyAndStoreCoin(
logger.warn("processPlanchet: planchet already withdrawn");
return;
}
return { planchet, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl };
const denomInfo = await ws.getDenomInfo(
ws,
tx,
withdrawalGroup.exchangeBaseUrl,
planchet.denomPubHash,
);
if (!denomInfo) {
return;
}
return {
planchet,
denomInfo,
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
};
});
if (!d) {
return;
}
const { planchet, exchangeBaseUrl } = d;
const { planchet, denomInfo } = d;
const planchetDenomPub = planchet.denomPub;
const planchetDenomPub = denomInfo.denomPub;
if (planchetDenomPub.cipher !== DenomKeyType.Rsa) {
throw Error(`cipher (${planchetDenomPub.cipher}) not supported`);
}
@ -623,9 +634,9 @@ async function processPlanchetVerifyAndStoreCoin(
}
let denomSig: UnblindedSignature;
if (planchet.denomPub.cipher === DenomKeyType.Rsa) {
if (planchetDenomPub.cipher === DenomKeyType.Rsa) {
denomSig = {
cipher: planchet.denomPub.cipher,
cipher: planchetDenomPub.cipher,
rsa_signature: denomSigRsa,
};
} else {
@ -636,12 +647,11 @@ async function processPlanchetVerifyAndStoreCoin(
blindingKey: planchet.blindingKey,
coinPriv: planchet.coinPriv,
coinPub: planchet.coinPub,
currentAmount: planchet.coinValue,
denomPub: planchet.denomPub,
currentAmount: denomInfo.value,
denomPubHash: planchet.denomPubHash,
denomSig,
coinEvHash: planchet.coinEvHash,
exchangeBaseUrl: exchangeBaseUrl,
exchangeBaseUrl: d.exchangeBaseUrl,
status: CoinStatus.Fresh,
coinSource: {
type: CoinSourceType.Withdraw,

View File

@ -77,6 +77,8 @@ export interface AvailableCoinInfo {
/**
* Coin's denomination public key.
*
* FIXME: We should only need the denomPubHash here, if at all.
*/
denomPub: DenominationPubKey;

View File

@ -24,23 +24,62 @@
*/
import {
AcceptManualWithdrawalResult,
AcceptWithdrawalResponse, AmountJson, Amounts, BalancesResponse, codecForAbortPayWithRefundRequest,
AcceptWithdrawalResponse,
AmountJson,
Amounts,
BalancesResponse,
codecForAbortPayWithRefundRequest,
codecForAcceptBankIntegratedWithdrawalRequest,
codecForAcceptExchangeTosRequest,
codecForAcceptManualWithdrawalRequet,
codecForAcceptTipRequest,
codecForAddExchangeRequest, codecForAny, codecForApplyRefundRequest,
codecForAddExchangeRequest,
codecForAny,
codecForApplyRefundRequest,
codecForConfirmPayRequest,
codecForCreateDepositGroupRequest, codecForDeleteTransactionRequest, codecForForceRefreshRequest,
codecForGetExchangeTosRequest, codecForGetExchangeWithdrawalInfo, codecForGetFeeForDeposit, codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri, codecForImportDbRequest, codecForIntegrationTestArgs, codecForListKnownBankAccounts, codecForPreparePayRequest,
codecForPrepareTipRequest, codecForRetryTransactionRequest, codecForSetCoinSuspendedRequest, codecForSetWalletDeviceIdRequest, codecForTestPayArgs,
codecForTrackDepositGroupRequest, codecForTransactionsRequest, codecForWithdrawFakebankRequest, codecForWithdrawTestBalance, CoinDumpJson, CoreApiResponse, durationFromSpec,
durationMin, ExchangeListItem,
ExchangesListRespose, getDurationRemaining, GetExchangeTosResult, isTimestampExpired,
j2s, KnownBankAccounts, Logger, ManualWithdrawalDetails, NotificationType, parsePaytoUri, PaytoUri, RefreshReason, TalerErrorCode,
codecForCreateDepositGroupRequest,
codecForDeleteTransactionRequest,
codecForForceRefreshRequest,
codecForGetExchangeTosRequest,
codecForGetExchangeWithdrawalInfo,
codecForGetFeeForDeposit,
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
codecForImportDbRequest,
codecForIntegrationTestArgs,
codecForListKnownBankAccounts,
codecForPreparePayRequest,
codecForPrepareTipRequest,
codecForRetryTransactionRequest,
codecForSetCoinSuspendedRequest,
codecForSetWalletDeviceIdRequest,
codecForTestPayArgs,
codecForTrackDepositGroupRequest,
codecForTransactionsRequest,
codecForWithdrawFakebankRequest,
codecForWithdrawTestBalance,
CoinDumpJson,
CoreApiResponse,
durationFromSpec,
durationMin,
ExchangeListItem,
ExchangesListRespose,
getDurationRemaining,
GetExchangeTosResult,
isTimestampExpired,
j2s,
KnownBankAccounts,
Logger,
ManualWithdrawalDetails,
NotificationType,
parsePaytoUri,
PaytoUri,
RefreshReason,
TalerErrorCode,
Timestamp,
timestampMin, URL, WalletNotification
timestampMin,
URL,
WalletNotification,
} from "@gnu-taler/taler-util";
import {
DenomInfo,
@ -50,7 +89,7 @@ import {
MerchantOperations,
NotificationListener,
RecoupOperations,
ReserveOperations
ReserveOperations,
} from "./common.js";
import { CryptoApi, CryptoWorkerFactory } from "./crypto/workers/cryptoApi.js";
import {
@ -59,12 +98,12 @@ import {
exportDb,
importDb,
ReserveRecordStatus,
WalletStoresV1
WalletStoresV1,
} from "./db.js";
import {
makeErrorDetails,
OperationFailedAndReportedError,
OperationFailedError
OperationFailedError,
} from "./errors.js";
import { exportBackup } from "./operations/backup/export.js";
import {
@ -77,7 +116,7 @@ import {
loadBackupRecovery,
processBackupForProvider,
removeBackupProvider,
runBackupCycle
runBackupCycle,
} from "./operations/backup/index.js";
import { setWalletDeviceId } from "./operations/backup/state.js";
import { getBalances } from "./operations/balance.js";
@ -85,7 +124,7 @@ import {
createDepositGroup,
getFeeForDeposit,
processDepositGroup,
trackDepositGroup
trackDepositGroup,
} from "./operations/deposits.js";
import {
acceptExchangeTermsOfService,
@ -94,62 +133,64 @@ import {
getExchangeRequestTimeout,
getExchangeTrust,
updateExchangeFromUrl,
updateExchangeTermsOfService
updateExchangeTermsOfService,
} from "./operations/exchanges.js";
import { getMerchantInfo } from "./operations/merchants.js";
import {
confirmPay,
preparePayForUri,
processDownloadProposal,
processPurchasePay
processPurchasePay,
} from "./operations/pay.js";
import { getPendingOperations } from "./operations/pending.js";
import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
import {
autoRefresh,
createRefreshGroup,
processRefreshGroup
processRefreshGroup,
} from "./operations/refresh.js";
import {
abortFailedPayWithRefund,
applyRefund,
processPurchaseQueryRefund
processPurchaseQueryRefund,
} from "./operations/refund.js";
import {
createReserve,
createTalerWithdrawReserve,
getFundingPaytoUris,
processReserve
processReserve,
} from "./operations/reserves.js";
import {
runIntegrationTest,
testPay,
withdrawTestBalance
withdrawTestBalance,
} from "./operations/testing.js";
import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
import {
deleteTransaction,
getTransactions,
retryTransaction
retryTransaction,
} from "./operations/transactions.js";
import {
getExchangeWithdrawalInfo,
getWithdrawalDetailsForUri,
processWithdrawGroup
processWithdrawGroup,
} from "./operations/withdraw.js";
import {
PendingOperationsResponse, PendingTaskInfo, PendingTaskType
PendingOperationsResponse,
PendingTaskInfo,
PendingTaskType,
} from "./pending-types.js";
import { assertUnreachable } from "./util/assertUnreachable.js";
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow
readSuccessResponseJsonOrThrow,
} from "./util/http.js";
import {
AsyncCondition,
OpenedPromise,
openPromise
openPromise,
} from "./util/promiseUtils.js";
import { DbAccess, GetReadWriteAccess } from "./util/query.js";
import { TimerGroup } from "./util/timer.js";
@ -455,7 +496,10 @@ async function getExchangeTos(
) {
throw Error("exchange is in invalid state");
}
if (acceptedFormat && acceptedFormat.findIndex(f => f === contentType) !== -1) {
if (
acceptedFormat &&
acceptedFormat.findIndex((f) => f === contentType) !== -1
) {
return {
acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
currentEtag,
@ -464,7 +508,12 @@ async function getExchangeTos(
};
}
const tosDownload = await downloadTosFromAcceptedFormat(ws, exchangeBaseUrl, getExchangeRequestTimeout(), acceptedFormat);
const tosDownload = await downloadTosFromAcceptedFormat(
ws,
exchangeBaseUrl,
getExchangeRequestTimeout(),
acceptedFormat,
);
if (tosDownload.tosContentType === contentType) {
return {
@ -474,7 +523,7 @@ async function getExchangeTos(
contentType,
};
}
await updateExchangeTermsOfService(ws, exchangeBaseUrl, tosDownload)
await updateExchangeTermsOfService(ws, exchangeBaseUrl, tosDownload);
return {
acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
@ -482,7 +531,6 @@ async function getExchangeTos(
content: tosDownload.tosText,
contentType: tosDownload.tosContentType,
};
}
async function listKnownBankAccounts(
@ -641,9 +689,15 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
}
withdrawalReservePub = ws.reservePub;
}
const denomInfo = await ws.getDenomInfo(
ws,
tx,
c.exchangeBaseUrl,
c.denomPubHash,
);
coinsJson.coins.push({
coin_pub: c.coinPub,
denom_pub: c.denomPub,
denom_pub: denomInfo?.denomPub!,
denom_pub_hash: c.denomPubHash,
denom_value: Amounts.stringify(denom.value),
exchange_base_url: c.exchangeBaseUrl,
@ -1030,7 +1084,7 @@ export async function handleCoreApiRequest(
try {
logger.error("Caught unexpected exception:");
logger.error(e.stack);
} catch (e) { }
} catch (e) {}
return {
type: "error",
operation,
@ -1236,7 +1290,10 @@ class InternalWalletStateImpl implements InternalWalletState {
* Run an async function after acquiring a list of locks, identified
* by string tokens.
*/
async runSequentialized<T>(tokens: string[], f: () => Promise<T>): Promise<T> {
async runSequentialized<T>(
tokens: string[],
f: () => Promise<T>,
): Promise<T> {
// Make sure locks are always acquired in the same order
tokens = [...tokens].sort();
@ -1269,4 +1326,3 @@ class InternalWalletStateImpl implements InternalWalletState {
}
}
}