summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/reserves.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2020-12-16 17:59:04 +0100
committerFlorian Dold <florian@dold.me>2020-12-16 17:59:04 +0100
commitbafb52edff4d56bcb9e3c3d0a260f507c517b08c (patch)
tree09b484a8cedc9893f5ea7593a98fadde075eafa3 /packages/taler-wallet-core/src/operations/reserves.ts
parentc09c5bbe625566fc61c811160d2ccdab263327fa (diff)
downloadwallet-core-bafb52edff4d56bcb9e3c3d0a260f507c517b08c.tar.gz
wallet-core-bafb52edff4d56bcb9e3c3d0a260f507c517b08c.tar.bz2
wallet-core-bafb52edff4d56bcb9e3c3d0a260f507c517b08c.zip
don't store reserve history anymore, adjust withdrawal implementation accordingly
Diffstat (limited to 'packages/taler-wallet-core/src/operations/reserves.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/reserves.ts367
1 files changed, 119 insertions, 248 deletions
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts
index a2a1b3018..95c38120c 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -28,8 +28,6 @@ import {
CurrencyRecord,
Stores,
WithdrawalGroupRecord,
- WalletReserveHistoryItemType,
- ReserveHistoryRecord,
ReserveBankInfo,
} from "../types/dbTypes";
import { Logger } from "../util/logging";
@@ -47,10 +45,12 @@ import { assertUnreachable } from "../util/assertUnreachable";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { randomBytes } from "../crypto/primitives/nacl-fast";
import {
- selectWithdrawalDenoms,
processWithdrawGroup,
getBankWithdrawalInfo,
denomSelectionInfoToState,
+ updateWithdrawalDenoms,
+ selectWithdrawalDenominations,
+ getPossibleWithdrawalDenoms,
} from "./withdraw";
import {
guardOperationException,
@@ -66,11 +66,6 @@ import {
durationMin,
durationMax,
} from "../util/time";
-import {
- reconcileReserveHistory,
- summarizeReserveHistory,
- ReserveHistorySummary,
-} from "../util/reserveHistoryUtil";
import { TransactionHandle } from "../util/query";
import { addPaytoQueryParams } from "../util/payto";
import { TalerErrorCode } from "../TalerErrorCode";
@@ -86,6 +81,7 @@ import {
getRetryDuration,
updateRetryInfoTimeout,
} from "../util/retries";
+import { ReserveTransactionType } from "../types/ReserveTransaction";
const logger = new Logger("reserves.ts");
@@ -138,11 +134,9 @@ export async function createReserve(
const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
- const denomSelInfo = await selectWithdrawalDenoms(
- ws,
- canonExchange,
- req.amount,
- );
+ await updateWithdrawalDenoms(ws, canonExchange);
+ const denoms = await getPossibleWithdrawalDenoms(ws, canonExchange);
+ const denomSelInfo = selectWithdrawalDenominations(req.amount, denoms);
const initialDenomSel = denomSelectionInfoToState(denomSelInfo);
const reserveRecord: ReserveRecord = {
@@ -166,16 +160,6 @@ export async function createReserve(
requestedQuery: false,
};
- const reserveHistoryRecord: ReserveHistoryRecord = {
- reservePub: keypair.pub,
- reserveTransactions: [],
- };
-
- reserveHistoryRecord.reserveTransactions.push({
- type: WalletReserveHistoryItemType.Credit,
- expectedAmount: req.amount,
- });
-
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
const exchangeDetails = exchangeInfo.details;
if (!exchangeDetails) {
@@ -206,12 +190,7 @@ export async function createReserve(
const cr: CurrencyRecord = currencyRecord;
const resp = await ws.db.runWithWriteTransaction(
- [
- Stores.currencies,
- Stores.reserves,
- Stores.reserveHistory,
- Stores.bankWithdrawUris,
- ],
+ [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris],
async (tx) => {
// Check if we have already created a reserve for that bankWithdrawStatusUrl
if (reserveRecord.bankInfo?.statusUrl) {
@@ -238,7 +217,6 @@ export async function createReserve(
}
await tx.put(Stores.currencies, cr);
await tx.put(Stores.reserves, reserveRecord);
- await tx.put(Stores.reserveHistory, reserveHistoryRecord);
const r: CreateReserveResponse = {
exchange: canonExchange,
reservePub: keypair.pub,
@@ -499,6 +477,10 @@ async function incrementReserveRetry(
/**
* Update the information about a reserve that is stored in the wallet
* by quering the reserve's exchange.
+ *
+ * If the reserve have funds that are not allocated in a withdrawal group yet
+ * and are big enough to withdraw with available denominations,
+ * create a new withdrawal group for the remaining amount.
*/
async function updateReserve(
ws: InternalWalletState,
@@ -542,78 +524,130 @@ async function updateReserve(
}
const reserveInfo = result.response;
-
const balance = Amounts.parseOrThrow(reserveInfo.balance);
const currency = balance.currency;
- let updateSummary: ReserveHistorySummary | undefined;
- await ws.db.runWithWriteTransaction(
- [Stores.reserves, Stores.reserveHistory],
+
+ await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
+ const denoms = await getPossibleWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
+
+ const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
+ [Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves],
async (tx) => {
- const r = await tx.get(Stores.reserves, reservePub);
- if (!r) {
- return;
- }
- if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
+ const newReserve = await tx.get(Stores.reserves, reserve.reservePub);
+ if (!newReserve) {
return;
}
+ let amountReservePlus = Amounts.getZero(currency);
+ let amountReserveMinus = Amounts.getZero(currency);
+
+ // Subtract withdrawal groups for this reserve from the available amount.
+ await tx
+ .iterIndexed(Stores.withdrawalGroups.byReservePub, reservePub)
+ .forEach((wg) => {
+ const cost = wg.denomsSel.totalWithdrawCost;
+ amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount;
+ });
- const hist = await tx.get(Stores.reserveHistory, reservePub);
- if (!hist) {
- throw Error("inconsistent database");
+ 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.getIndexed(
+ Stores.planchets.coinEvHashIndex,
+ entry.h_coin_envelope,
+ );
+ if (planchet) {
+ // Amount is already accounted in some withdrawal session
+ break;
+ }
+ const coin = await tx.getIndexed(
+ Stores.coins.coinEvHashIndex,
+ 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;
+ }
+ }
}
- const newHistoryTransactions = reserveInfo.history.slice(
- hist.reserveTransactions.length,
+ const remainingAmount = Amounts.sub(amountReservePlus, amountReserveMinus)
+ .amount;
+ const denomSelInfo = selectWithdrawalDenominations(
+ remainingAmount,
+ denoms,
);
- const reserveUpdateId = encodeCrock(getRandomBytes(32));
+ if (denomSelInfo.selectedDenoms.length > 0) {
+ let withdrawalGroupId: string;
- const reconciled = reconcileReserveHistory(
- hist.reserveTransactions,
- reserveInfo.history,
- );
-
- updateSummary = summarizeReserveHistory(
- reconciled.updatedLocalHistory,
- currency,
- );
-
- if (
- reconciled.newAddedItems.length + reconciled.newMatchedItems.length !=
- 0
- ) {
- logger.trace("setting reserve status to 'withdrawing' after query");
- r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
- r.retryInfo = initRetryInfo();
- r.requestedQuery = false;
- } else {
- if (r.requestedQuery) {
- logger.trace(
- "setting reserve status to 'querying-status' (requested query) after query",
- );
- r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
- r.requestedQuery = false;
- r.retryInfo = initRetryInfo();
+ if (!newReserve.initialWithdrawalStarted) {
+ withdrawalGroupId = newReserve.initialWithdrawalGroupId;
+ newReserve.initialWithdrawalStarted = true;
} else {
- logger.trace("setting reserve status to 'dormant' after query");
- r.reserveStatus = ReserveRecordStatus.DORMANT;
- r.retryInfo = initRetryInfo(false);
+ withdrawalGroupId = encodeCrock(randomBytes(32));
}
+
+ const withdrawalRecord: WithdrawalGroupRecord = {
+ withdrawalGroupId: withdrawalGroupId,
+ exchangeBaseUrl: reserve.exchangeBaseUrl,
+ reservePub: reserve.reservePub,
+ rawWithdrawalAmount: remainingAmount,
+ timestampStart: getTimestampNow(),
+ retryInfo: initRetryInfo(),
+ lastError: undefined,
+ denomsSel: denomSelectionInfoToState(denomSelInfo),
+ };
+
+ newReserve.lastError = undefined;
+ newReserve.retryInfo = initRetryInfo(false);
+ newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
+
+ await tx.put(Stores.reserves, newReserve);
+ await tx.put(Stores.withdrawalGroups, withdrawalRecord);
+ return withdrawalRecord;
}
- r.lastSuccessfulStatusQuery = getTimestampNow();
- hist.reserveTransactions = reconciled.updatedLocalHistory;
- r.lastError = undefined;
- await tx.put(Stores.reserves, r);
- await tx.put(Stores.reserveHistory, hist);
+ return;
},
);
- ws.notify({ type: NotificationType.ReserveUpdated, updateSummary });
- const reserve2 = await ws.db.get(Stores.reserves, reservePub);
- if (reserve2) {
- logger.trace(
- `after db transaction, reserve status is ${reserve2.reserveStatus}`,
- );
+
+ if (newWithdrawalGroup) {
+ logger.trace("processing new withdraw group");
+ ws.notify({
+ type: NotificationType.WithdrawGroupCreated,
+ withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
+ });
+ await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
+ } else {
+ console.trace("withdraw session already existed");
}
+
return { ready: true };
}
@@ -651,9 +685,6 @@ async function processReserveImpl(
break;
}
}
- case ReserveRecordStatus.WITHDRAWING:
- await depleteReserve(ws, reservePub);
- break;
case ReserveRecordStatus.DORMANT:
// nothing to do
break;
@@ -669,166 +700,6 @@ async function processReserveImpl(
}
}
-/**
- * Withdraw coins from a reserve until it is empty.
- *
- * When finished, marks the reserve as depleted by setting
- * the depleted timestamp.
- */
-async function depleteReserve(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- let reserve: ReserveRecord | undefined;
- let hist: ReserveHistoryRecord | undefined;
- await ws.db.runWithReadTransaction(
- [Stores.reserves, Stores.reserveHistory],
- async (tx) => {
- reserve = await tx.get(Stores.reserves, reservePub);
- hist = await tx.get(Stores.reserveHistory, reservePub);
- },
- );
-
- if (!reserve) {
- return;
- }
- if (!hist) {
- throw Error("inconsistent database");
- }
- if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
- return;
- }
- logger.trace(`depleting reserve ${reservePub}`);
-
- const summary = summarizeReserveHistory(
- hist.reserveTransactions,
- reserve.currency,
- );
-
- const withdrawAmount = summary.unclaimedReserveAmount;
-
- const denomsForWithdraw = await selectWithdrawalDenoms(
- ws,
- reserve.exchangeBaseUrl,
- withdrawAmount,
- );
- if (!denomsForWithdraw) {
- // Only complain about inability to withdraw if we
- // didn't withdraw before.
- if (Amounts.isZero(summary.withdrawnAmount)) {
- const opErr = makeErrorDetails(
- TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
- `Unable to withdraw from reserve, no denominations are available to withdraw.`,
- {},
- );
- await incrementReserveRetry(ws, reserve.reservePub, opErr);
- throw new OperationFailedAndReportedError(opErr);
- }
- return;
- }
-
- logger.trace(
- `Selected coins total cost ${Amounts.stringify(
- denomsForWithdraw.totalWithdrawCost,
- )} for withdrawal of ${Amounts.stringify(withdrawAmount)}`,
- );
-
- logger.trace("selected denominations");
-
- const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
- [
- Stores.withdrawalGroups,
- Stores.reserves,
- Stores.reserveHistory,
- Stores.planchets,
- ],
- async (tx) => {
- const newReserve = await tx.get(Stores.reserves, reservePub);
- if (!newReserve) {
- return false;
- }
- if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
- return false;
- }
- const newHist = await tx.get(Stores.reserveHistory, reservePub);
- if (!newHist) {
- throw Error("inconsistent database");
- }
- const newSummary = summarizeReserveHistory(
- newHist.reserveTransactions,
- newReserve.currency,
- );
- if (
- Amounts.cmp(
- newSummary.unclaimedReserveAmount,
- denomsForWithdraw.totalWithdrawCost,
- ) < 0
- ) {
- // Something must have happened concurrently!
- logger.error(
- "aborting withdrawal session, likely concurrent withdrawal happened",
- );
- logger.error(
- `unclaimed reserve amount is ${newSummary.unclaimedReserveAmount}`,
- );
- logger.error(
- `withdrawal cost is ${denomsForWithdraw.totalWithdrawCost}`,
- );
- return false;
- }
- for (let i = 0; i < denomsForWithdraw.selectedDenoms.length; i++) {
- const sd = denomsForWithdraw.selectedDenoms[i];
- for (let j = 0; j < sd.count; j++) {
- const amt = Amounts.add(sd.denom.value, sd.denom.feeWithdraw).amount;
- newHist.reserveTransactions.push({
- type: WalletReserveHistoryItemType.Withdraw,
- expectedAmount: amt,
- });
- }
- }
- logger.trace("setting reserve status to dormant after depletion");
- newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
- newReserve.retryInfo = initRetryInfo(false);
-
- let withdrawalGroupId: string;
-
- if (!newReserve.initialWithdrawalStarted) {
- withdrawalGroupId = newReserve.initialWithdrawalGroupId;
- newReserve.initialWithdrawalStarted = true;
- } else {
- withdrawalGroupId = encodeCrock(randomBytes(32));
- }
-
- const withdrawalRecord: WithdrawalGroupRecord = {
- withdrawalGroupId: withdrawalGroupId,
- exchangeBaseUrl: newReserve.exchangeBaseUrl,
- reservePub: newReserve.reservePub,
- rawWithdrawalAmount: withdrawAmount,
- timestampStart: getTimestampNow(),
- retryInfo: initRetryInfo(),
- lastError: undefined,
- denomsSel: denomSelectionInfoToState(denomsForWithdraw),
- };
-
- await tx.put(Stores.reserves, newReserve);
- await tx.put(Stores.reserveHistory, newHist);
- await tx.put(Stores.withdrawalGroups, withdrawalRecord);
- return withdrawalRecord;
- },
- );
-
- if (newWithdrawalGroup) {
- logger.trace("processing new withdraw group");
- ws.notify({
- type: NotificationType.WithdrawGroupCreated,
- withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
- });
- await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
- } else {
- console.trace("withdraw session already existed");
- }
-}
-
export async function createTalerWithdrawReserve(
ws: InternalWalletState,
talerWithdrawUri: string,