don't store reserve history anymore, adjust withdrawal implementation accordingly

This commit is contained in:
Florian Dold 2020-12-16 17:59:04 +01:00
parent c09c5bbe62
commit bafb52edff
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
24 changed files with 253 additions and 1754 deletions

View File

@ -206,6 +206,7 @@ export class CryptoImplementation {
const tipPlanchet: DerivedTipPlanchet = {
blindingKey: encodeCrock(blindingFactor),
coinEv: encodeCrock(ev),
coinEvHash: encodeCrock(hash(ev)),
coinPriv: encodeCrock(fc.coinPriv),
coinPub: encodeCrock(fc.coinPub),
};
@ -463,6 +464,7 @@ export class CryptoImplementation {
coinEv: encodeCrock(ev),
privateKey: encodeCrock(coinPriv),
publicKey: encodeCrock(coinPub),
coinEvHash: encodeCrock(hash(ev)),
};
planchets.push(planchet);

View File

@ -63,5 +63,5 @@ export * from "./util/time";
export * from "./types/talerTypes";
export * from "./types/walletTypes";
export * from "./types/notifications";
export * from "./types/transactions";
export * from "./types/pending";
export * from "./types/transactionsTypes";
export * from "./types/pendingTypes";

View File

@ -44,6 +44,7 @@ import {
BackupRefundState,
BackupReserve,
BackupTip,
BackupWithdrawalGroup,
WalletBackupContentV1,
} from "../types/backupTypes";
import { TransactionHandle } from "../util/query";
@ -172,6 +173,7 @@ export async function exportBackup(
Stores.tips,
Stores.recoupGroups,
Stores.reserves,
Stores.withdrawalGroups,
],
async (tx) => {
const bs = await getWalletBackupState(ws, tx);
@ -188,9 +190,46 @@ export async function exportBackup(
const backupBackupProviders: BackupBackupProvider[] = [];
const backupTips: BackupTip[] = [];
const backupRecoupGroups: BackupRecoupGroup[] = [];
const withdrawalGroupsByReserve: {
[reservePub: string]: BackupWithdrawalGroup[];
} = {};
await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wg) => {
const withdrawalGroups = (withdrawalGroupsByReserve[
wg.reservePub
] ??= []);
// FIXME: finish!
// withdrawalGroups.push({
// raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount),
// selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({
// count: x.count,
// denom_pub_hash: x.denomPubHash,
// })),
// timestamp_start: wg.timestampStart,
// timestamp_finish: wg.timestampFinish,
// withdrawal_group_id: wg.withdrawalGroupId,
// });
});
await tx.iter(Stores.reserves).forEach((reserve) => {
// FIXME: implement
const backupReserve: BackupReserve = {
initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map(
(x) => ({
count: x.count,
denom_pub_hash: x.denomPubHash,
}),
),
initial_withdrawal_group_id: reserve.initialWithdrawalGroupId,
instructed_amount: Amounts.stringify(reserve.instructedAmount),
reserve_priv: reserve.reservePriv,
timestamp_created: reserve.timestampCreated,
withdrawal_groups:
withdrawalGroupsByReserve[reserve.reservePub] ?? [],
};
const backupReserves = (backupReservesByExchange[
reserve.exchangeBaseUrl
] ??= []);
backupReserves.push(backupReserve);
});
await tx.iter(Stores.tips).forEach((tip) => {

View File

@ -58,7 +58,6 @@ import {
} from "../util/http";
import { Logger } from "../util/logging";
import { URL } from "../util/url";
import { reconcileReserveHistory } from "../util/reserveHistoryUtil";
import { checkDbInvariant } from "../util/invariants";
import { NotificationType } from "../types/notifications";
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";

View File

@ -460,6 +460,8 @@ async function recordConfirmPay(
paymentSubmitPending: true,
refunds: {},
merchantPaySig: undefined,
noncePriv: proposal.noncePriv,
noncePub: proposal.noncePub,
};
await ws.db.runWithWriteTransaction(

View File

@ -29,7 +29,7 @@ import {
PendingOperationType,
ExchangeUpdateOperationStage,
ReserveType,
} from "../types/pending";
} from "../types/pendingTypes";
import {
Duration,
getTimestampNow,
@ -189,7 +189,6 @@ async function gatherReservePending(
// nothing to report as pending
break;
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
case ReserveRecordStatus.WITHDRAWING:
case ReserveRecordStatus.QUERYING_STATUS:
case ReserveRecordStatus.REGISTERING_BANK:
resp.nextRetryDelay = updateRetryDelay(

View File

@ -29,7 +29,7 @@ import { amountToPretty } from "../util/helpers";
import { TransactionHandle } from "../util/query";
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
import { Logger } from "../util/logging";
import { getWithdrawDenomList, isWithdrawableDenom } from "./withdraw";
import { selectWithdrawalDenominations, isWithdrawableDenom } from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges";
import {
TalerErrorDetails,
@ -83,7 +83,7 @@ export function getTotalRefreshCost(
): AmountJson {
const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh)
.amount;
const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms);
const withdrawDenoms = selectWithdrawalDenominations(withdrawAmount, denoms);
const resultingAmount = Amounts.add(
Amounts.getZero(withdrawAmount.currency),
...withdrawDenoms.selectedDenoms.map(
@ -150,7 +150,7 @@ async function refreshCreateSession(
oldDenom.feeRefresh,
).amount;
const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms);
const newCoinDenoms = selectWithdrawalDenominations(availableAmount, availableDenoms);
if (newCoinDenoms.selectedDenoms.length === 0) {
logger.trace(
@ -478,6 +478,7 @@ async function refreshReveal(
oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
},
suspended: false,
coinEvHash: pc.coinEv,
};
coins.push(coin);

View File

@ -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);
const hist = await tx.get(Stores.reserveHistory, reservePub);
if (!hist) {
throw Error("inconsistent database");
}
// 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 newHistoryTransactions = reserveInfo.history.slice(
hist.reserveTransactions.length,
);
const reserveUpdateId = encodeCrock(getRandomBytes(32));
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();
} else {
logger.trace("setting reserve status to 'dormant' after query");
r.reserveStatus = ReserveRecordStatus.DORMANT;
r.retryInfo = initRetryInfo(false);
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;
}
}
}
r.lastSuccessfulStatusQuery = getTimestampNow();
hist.reserveTransactions = reconciled.updatedLocalHistory;
r.lastError = undefined;
await tx.put(Stores.reserves, r);
await tx.put(Stores.reserveHistory, hist);
const remainingAmount = Amounts.sub(amountReservePlus, amountReserveMinus)
.amount;
const denomSelInfo = selectWithdrawalDenominations(
remainingAmount,
denoms,
);
if (denomSelInfo.selectedDenoms.length > 0) {
let withdrawalGroupId: string;
if (!newReserve.initialWithdrawalStarted) {
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
newReserve.initialWithdrawalStarted = true;
} else {
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;
}
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,

View File

@ -19,7 +19,7 @@ import { BalancesResponse } from "../types/walletTypes";
import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi";
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo";
import { Logger } from "../util/logging";
import { PendingOperationsResponse } from "../types/pending";
import { PendingOperationsResponse } from "../types/pendingTypes";
import { WalletNotification } from "../types/notifications";
import { Database } from "../util/query";
import { openPromise, OpenedPromise } from "../util/promiseUtils";

View File

@ -32,8 +32,10 @@ import {
} from "../types/dbTypes";
import {
getExchangeWithdrawalInfo,
selectWithdrawalDenoms,
denomSelectionInfoToState,
updateWithdrawalDenoms,
getPossibleWithdrawalDenoms,
selectWithdrawalDenominations,
} from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
@ -92,12 +94,15 @@ export async function prepareTip(
);
const walletTipId = encodeCrock(getRandomBytes(32));
const selectedDenoms = await selectWithdrawalDenoms(
ws,
tipPickupStatus.exchange_url,
await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
const denoms = await getPossibleWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
const selectedDenoms = await selectWithdrawalDenominations(
amount,
denoms
);
const secretSeed = encodeCrock(getRandomBytes(64));
tipRecord = {
walletTipId: walletTipId,
acceptedTimestamp: undefined,
@ -105,7 +110,6 @@ export async function prepareTip(
tipExpiration: tipPickupStatus.expiration,
exchangeBaseUrl: tipPickupStatus.exchange_url,
merchantBaseUrl: res.merchantBaseUrl,
planchets: undefined,
createdTimestamp: getTimestampNow(),
merchantTipId: res.merchantTipId,
tipAmountEffective: Amounts.sub(
@ -117,6 +121,7 @@ export async function prepareTip(
lastError: undefined,
denomsSel: denomSelectionInfoToState(selectedDenoms),
pickedUpTimestamp: undefined,
secretSeed,
};
await ws.db.put(Stores.tips, tipRecord);
}
@ -316,6 +321,7 @@ async function processTipImpl(
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
status: CoinStatus.Fresh,
suspended: false,
coinEvHash: planchet.coinEvHash,
});
}

View File

@ -36,7 +36,7 @@ import {
WithdrawalType,
WithdrawalDetails,
OrderShortInfo,
} from "../types/transactions";
} from "../types/transactionsTypes";
import { getFundingPaytoUris } from "./reserves";
import { TipResponse } from "../types/talerTypes";

View File

@ -15,7 +15,7 @@
*/
import test from "ava";
import { getWithdrawDenomList } from "./withdraw";
import { selectWithdrawalDenominations } from "./withdraw";
import { Amounts } from "../util/amounts";
test("withdrawal selection bug repro", (t) => {
@ -322,7 +322,7 @@ test("withdrawal selection bug repro", (t) => {
},
];
const res = getWithdrawDenomList(amount, denoms);
const res = selectWithdrawalDenominations(amount, denoms);
console.error("cost", Amounts.stringify(res.totalWithdrawCost));
console.error("withdraw amount", Amounts.stringify(amount));

View File

@ -86,12 +86,13 @@ export function isWithdrawableDenom(d: DenominationRecord): boolean {
return started && stillOkay && !d.isRevoked;
}
/**
* 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 getWithdrawDenomList(
export function selectWithdrawalDenominations(
amountAvailable: AmountJson,
denoms: DenominationRecord[],
): DenominationSelectionInfo {
@ -207,7 +208,7 @@ export async function getBankWithdrawalInfo(
/**
* Return denominations that can potentially used for a withdrawal.
*/
async function getPossibleDenoms(
export async function getPossibleWithdrawalDenoms(
ws: InternalWalletState,
exchangeBaseUrl: string,
): Promise<DenominationRecord[]> {
@ -470,6 +471,7 @@ async function processPlanchetVerifyAndStoreCoin(
denomPub: planchet.denomPub,
denomPubHash: planchet.denomPubHash,
denomSig,
coinEvHash: planchet.coinEvHash,
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
status: CoinStatus.Fresh,
coinSource: {
@ -524,17 +526,13 @@ export function denomSelectionInfoToState(
}
/**
* Get a list of denominations to withdraw from the given exchange for the
* given amount, making sure that all denominations' signatures are verified.
*
* Writes to the DB in order to record the result from verifying
* denominations.
* Make sure that denominations that currently can be used for withdrawal
* are validated, and the result of validation is stored in the database.
*/
export async function selectWithdrawalDenoms(
export async function updateWithdrawalDenoms(
ws: InternalWalletState,
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<DenominationSelectionInfo> {
): Promise<void> {
const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
if (!exchange) {
logger.error("exchange not found");
@ -545,43 +543,24 @@ export async function selectWithdrawalDenoms(
logger.error("exchange details not available");
throw Error(`exchange ${exchangeBaseUrl} details not available`);
}
let allValid = false;
let selectedDenoms: DenominationSelectionInfo;
// Find a denomination selection for the requested amount.
// If a selected denomination has not been validated yet
// and turns our to be invalid, we try again with the
// reduced set of denominations.
do {
allValid = true;
const nextPossibleDenoms = await getPossibleDenoms(ws, exchange.baseUrl);
selectedDenoms = getWithdrawDenomList(amount, nextPossibleDenoms);
for (const denomSel of selectedDenoms.selectedDenoms) {
const denom = denomSel.denom;
if (denom.status === DenominationStatus.Unverified) {
const valid = await ws.cryptoApi.isValidDenom(
denom,
exchangeDetails.masterPublicKey,
);
if (!valid) {
denom.status = DenominationStatus.VerifiedBad;
allValid = false;
} else {
denom.status = DenominationStatus.VerifiedGood;
}
await ws.db.put(Stores.denominations, denom);
const denominations = await getPossibleWithdrawalDenoms(ws, exchangeBaseUrl);
for (const denom of denominations) {
if (denom.status === DenominationStatus.Unverified) {
const valid = await ws.cryptoApi.isValidDenom(
denom,
exchangeDetails.masterPublicKey,
);
if (!valid) {
denom.status = DenominationStatus.VerifiedBad;
} else {
denom.status = DenominationStatus.VerifiedGood;
}
await ws.db.put(Stores.denominations, denom);
}
} while (selectedDenoms.selectedDenoms.length > 0 && !allValid);
if (Amounts.cmp(selectedDenoms.totalWithdrawCost, amount) > 0) {
throw Error("Bug: withdrawal coin selection is wrong");
}
return selectedDenoms;
}
async function incrementWithdrawalRetry(
ws: InternalWalletState,
withdrawalGroupId: string,
@ -745,7 +724,9 @@ export async function getExchangeWithdrawalInfo(
throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`);
}
const selectedDenoms = await selectWithdrawalDenoms(ws, baseUrl, amount);
await updateWithdrawalDenoms(ws, baseUrl);
const denoms = await getPossibleWithdrawalDenoms(ws, baseUrl);
const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
const exchangeWireAccounts: string[] = [];
for (const account of exchangeWireInfo.accounts) {
exchangeWireAccounts.push(account.payto_uri);

View File

@ -53,12 +53,6 @@
* Imports.
*/
import { Timestamp } from "../util/time";
import {
ReserveClosingTransaction,
ReserveCreditTransaction,
ReserveRecoupTransaction,
ReserveWithdrawTransaction,
} from "./ReserveTransaction";
/**
* Type alias for strings that are to be treated like amounts.
@ -1128,82 +1122,6 @@ export interface BackupExchange {
tos_etag_accepted: string | undefined;
}
export enum WalletReserveHistoryItemType {
Credit = "credit",
Withdraw = "withdraw",
Closing = "closing",
Recoup = "recoup",
}
export interface BackupReserveHistoryCreditItem {
type: WalletReserveHistoryItemType.Credit;
/**
* Amount we expect to see credited.
*/
expected_amount?: BackupAmountString;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matched_exchange_transaction?: ReserveCreditTransaction;
}
/**
* Reserve history item for a withdrawal
*/
export interface BackupReserveHistoryWithdrawItem {
type: WalletReserveHistoryItemType.Withdraw;
expected_amount?: BackupAmountString;
/**
* Hash of the blinded coin.
*
* When this value is set, it indicates that a withdrawal is active
* in the wallet for the reserve.
*/
expected_coin_ev_hash?: string;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matched_exchange_transaction?: ReserveWithdrawTransaction;
}
export interface BackupReserveHistoryClosingItem {
type: WalletReserveHistoryItemType.Closing;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matched_exchange_transaction?: ReserveClosingTransaction;
}
export interface BackupReserveHistoryRecoupItem {
type: WalletReserveHistoryItemType.Recoup;
/**
* Amount we expect to see recouped.
*/
expected_amount?: BackupAmountString;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matched_exchange_transaction?: ReserveRecoupTransaction;
}
export type BackupReserveHistoryItem =
| BackupReserveHistoryCreditItem
| BackupReserveHistoryWithdrawItem
| BackupReserveHistoryRecoupItem
| BackupReserveHistoryClosingItem;
export enum BackupProposalStatus {
/**
* Proposed (and either downloaded or not,

View File

@ -83,6 +83,11 @@ export interface DerivedRefreshSession {
*/
coinEv: string;
/**
* Hash of the blinded public key.
*/
coinEvHash: string;
/**
* Blinding key used.
*/
@ -122,6 +127,7 @@ export interface DeriveTipRequest {
export interface DerivedTipPlanchet {
blindingKey: string;
coinEv: string;
coinEvHash: string;
coinPriv: string;
coinPub: string;
}

View File

@ -65,12 +65,6 @@ export enum ReserveRecordStatus {
*/
QUERYING_STATUS = "querying-status",
/**
* Status is queried, the wallet must now select coins
* and start withdrawing.
*/
WITHDRAWING = "withdrawing",
/**
* The corresponding withdraw record has been created.
* No further processing is done, unless explicitly requested
@ -84,76 +78,6 @@ export enum ReserveRecordStatus {
BANK_ABORTED = "bank-aborted",
}
export enum WalletReserveHistoryItemType {
Credit = "credit",
Withdraw = "withdraw",
Closing = "closing",
Recoup = "recoup",
}
export interface WalletReserveHistoryCreditItem {
type: WalletReserveHistoryItemType.Credit;
/**
* Amount we expect to see credited.
*/
expectedAmount?: AmountJson;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matchedExchangeTransaction?: ReserveCreditTransaction;
}
export interface WalletReserveHistoryWithdrawItem {
expectedAmount?: AmountJson;
type: WalletReserveHistoryItemType.Withdraw;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matchedExchangeTransaction?: ReserveWithdrawTransaction;
}
export interface WalletReserveHistoryClosingItem {
type: WalletReserveHistoryItemType.Closing;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matchedExchangeTransaction?: ReserveClosingTransaction;
}
export interface WalletReserveHistoryRecoupItem {
type: WalletReserveHistoryItemType.Recoup;
/**
* Amount we expect to see recouped.
*/
expectedAmount?: AmountJson;
/**
* Item from the reserve transaction history that this
* wallet reserve history item matches up with.
*/
matchedExchangeTransaction?: ReserveRecoupTransaction;
}
export type WalletReserveHistoryItem =
| WalletReserveHistoryCreditItem
| WalletReserveHistoryWithdrawItem
| WalletReserveHistoryRecoupItem
| WalletReserveHistoryClosingItem;
export interface ReserveHistoryRecord {
reservePub: string;
reserveTransactions: WalletReserveHistoryItem[];
}
export interface ReserveBankInfo {
/**
* Status URL that the wallet will use to query the status
@ -667,6 +591,8 @@ export interface RefreshPlanchet {
*/
coinEv: string;
coinEvHash: string;
/**
* Blinding key used.
*/
@ -782,6 +708,14 @@ export interface CoinRecord {
*/
blindingKey: string;
/**
* Hash of the coin envelope.
*
* Stored here for indexing purposes, so that when looking at a
* reserve history, we can quickly find the coin for a withdrawal transaction.
*/
coinEvHash: string;
/**
* Status of the coin.
*/
@ -1536,6 +1470,12 @@ class CoinsStore extends Store<"coins", CoinRecord> {
string,
CoinRecord
>(this, "denomPubHashIndex", "denomPubHash");
coinEvHashIndex = new Index<"coins", "coinEvHashIndex", string, CoinRecord>(
this,
"coinEvHashIndex",
"coinEvHash",
);
}
class ProposalsStore extends Store<"proposals", ProposalRecord> {
@ -1602,15 +1542,6 @@ class ReservesStore extends Store<"reserves", ReserveRecord> {
}
}
class ReserveHistoryStore extends Store<
"reserveHistory",
ReserveHistoryRecord
> {
constructor() {
super("reserveHistory", { keyPath: "reservePub" });
}
}
class TipsStore extends Store<"tips", TipRecord> {
constructor() {
super("tips", { keyPath: "walletTipId" });
@ -1638,6 +1569,12 @@ class WithdrawalGroupsStore extends Store<
constructor() {
super("withdrawals", { keyPath: "withdrawalGroupId" });
}
byReservePub = new Index<
"withdrawals",
"withdrawalsByReserveIndex",
string,
WithdrawalGroupRecord
>(this, "withdrawalsByReserveIndex", "reservePub");
}
class PlanchetsStore extends Store<"planchets", PlanchetRecord> {
@ -1656,6 +1593,12 @@ class PlanchetsStore extends Store<"planchets", PlanchetRecord> {
string,
PlanchetRecord
>(this, "withdrawalGroupIndex", "withdrawalGroupId");
coinEvHashIndex = new Index<"planchets", "coinEvHashIndex", string, PlanchetRecord>(
this,
"coinEvHashIndex",
"coinEvHash",
);
}
/**
@ -1702,7 +1645,6 @@ export const Stores = {
keyPath: "recoupGroupId",
}),
reserves: new ReservesStore(),
reserveHistory: new ReserveHistoryStore(),
purchases: new PurchasesStore(),
tips: new TipsStore(),
withdrawalGroups: new WithdrawalGroupsStore(),

View File

@ -23,7 +23,6 @@
* Imports.
*/
import { TalerErrorDetails } from "./walletTypes";
import { ReserveHistorySummary } from "../util/reserveHistoryUtil";
export enum NotificationType {
CoinWithdrawn = "coin-withdrawn",
@ -125,10 +124,6 @@ export interface RefreshRefusedNotification {
type: NotificationType.RefreshUnwarranted;
}
export interface ReserveUpdatedNotification {
type: NotificationType.ReserveUpdated;
updateSummary?: ReserveHistorySummary;
}
export interface ReserveConfirmedNotification {
type: NotificationType.ReserveConfirmed;
@ -252,7 +247,6 @@ export type WalletNotification =
| RefreshRevealedNotification
| RefreshStartedNotification
| RefreshRefusedNotification
| ReserveUpdatedNotification
| ReserveCreatedNotification
| ReserveConfirmedNotification
| WithdrawalGroupFinishedNotification

View File

@ -1,276 +0,0 @@
/*
This file is part of GNU Taler
(C) 2019 GNUnet e.V.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Type and schema definitions for pending operations in the wallet.
*/
/**
* Imports.
*/
import { TalerErrorDetails, BalancesResponse } from "./walletTypes";
import { ReserveRecordStatus } from "./dbTypes";
import { Timestamp, Duration } from "../util/time";
import { RetryInfo } from "../util/retries";
export enum PendingOperationType {
Bug = "bug",
ExchangeUpdate = "exchange-update",
ExchangeCheckRefresh = "exchange-check-refresh",
Pay = "pay",
ProposalChoice = "proposal-choice",
ProposalDownload = "proposal-download",
Refresh = "refresh",
Reserve = "reserve",
Recoup = "recoup",
RefundQuery = "refund-query",
TipChoice = "tip-choice",
TipPickup = "tip-pickup",
Withdraw = "withdraw",
}
/**
* Information about a pending operation.
*/
export type PendingOperationInfo = PendingOperationInfoCommon &
(
| PendingBugOperation
| PendingExchangeUpdateOperation
| PendingExchangeCheckRefreshOperation
| PendingPayOperation
| PendingProposalChoiceOperation
| PendingProposalDownloadOperation
| PendingRefreshOperation
| PendingRefundQueryOperation
| PendingReserveOperation
| PendingTipChoiceOperation
| PendingTipPickupOperation
| PendingWithdrawOperation
| PendingRecoupOperation
);
/**
* The wallet is currently updating information about an exchange.
*/
export interface PendingExchangeUpdateOperation {
type: PendingOperationType.ExchangeUpdate;
stage: ExchangeUpdateOperationStage;
reason: string;
exchangeBaseUrl: string;
lastError: TalerErrorDetails | undefined;
}
/**
* The wallet should check whether coins from this exchange
* need to be auto-refreshed.
*/
export interface PendingExchangeCheckRefreshOperation {
type: PendingOperationType.ExchangeCheckRefresh;
exchangeBaseUrl: string;
}
/**
* Some interal error happened in the wallet. This pending operation
* should *only* be reported for problems in the wallet, not when
* a problem with a merchant/exchange/etc. occurs.
*/
export interface PendingBugOperation {
type: PendingOperationType.Bug;
message: string;
details: any;
}
/**
* Current state of an exchange update operation.
*/
export enum ExchangeUpdateOperationStage {
FetchKeys = "fetch-keys",
FetchWire = "fetch-wire",
FinalizeUpdate = "finalize-update",
}
export enum ReserveType {
/**
* Manually created.
*/
Manual = "manual",
/**
* Withdrawn from a bank that has "tight" Taler integration
*/
TalerBankWithdraw = "taler-bank-withdraw",
}
/**
* Status of processing a reserve.
*
* Does *not* include the withdrawal operation that might result
* from this.
*/
export interface PendingReserveOperation {
type: PendingOperationType.Reserve;
retryInfo: RetryInfo | undefined;
stage: ReserveRecordStatus;
timestampCreated: Timestamp;
reserveType: ReserveType;
reservePub: string;
bankWithdrawConfirmUrl?: string;
}
/**
* Status of an ongoing withdrawal operation.
*/
export interface PendingRefreshOperation {
type: PendingOperationType.Refresh;
lastError?: TalerErrorDetails;
refreshGroupId: string;
finishedPerCoin: boolean[];
retryInfo: RetryInfo;
}
/**
* Status of downloading signed contract terms from a merchant.
*/
export interface PendingProposalDownloadOperation {
type: PendingOperationType.ProposalDownload;
merchantBaseUrl: string;
proposalTimestamp: Timestamp;
proposalId: string;
orderId: string;
lastError?: TalerErrorDetails;
retryInfo: RetryInfo;
}
/**
* User must choose whether to accept or reject the merchant's
* proposed contract terms.
*/
export interface PendingProposalChoiceOperation {
type: PendingOperationType.ProposalChoice;
merchantBaseUrl: string;
proposalTimestamp: Timestamp;
proposalId: string;
}
/**
* The wallet is picking up a tip that the user has accepted.
*/
export interface PendingTipPickupOperation {
type: PendingOperationType.TipPickup;
tipId: string;
merchantBaseUrl: string;
merchantTipId: string;
}
/**
* The wallet has been offered a tip, and the user now needs to
* decide whether to accept or reject the tip.
*/
export interface PendingTipChoiceOperation {
type: PendingOperationType.TipChoice;
tipId: string;
merchantBaseUrl: string;
merchantTipId: string;
}
/**
* The wallet is signing coins and then sending them to
* the merchant.
*/
export interface PendingPayOperation {
type: PendingOperationType.Pay;
proposalId: string;
isReplay: boolean;
retryInfo: RetryInfo;
lastError: TalerErrorDetails | undefined;
}
/**
* The wallet is querying the merchant about whether any refund
* permissions are available for a purchase.
*/
export interface PendingRefundQueryOperation {
type: PendingOperationType.RefundQuery;
proposalId: string;
retryInfo: RetryInfo;
lastError: TalerErrorDetails | undefined;
}
export interface PendingRecoupOperation {
type: PendingOperationType.Recoup;
recoupGroupId: string;
retryInfo: RetryInfo;
lastError: TalerErrorDetails | undefined;
}
/**
* Status of an ongoing withdrawal operation.
*/
export interface PendingWithdrawOperation {
type: PendingOperationType.Withdraw;
lastError: TalerErrorDetails | undefined;
retryInfo: RetryInfo;
withdrawalGroupId: string;
numCoinsWithdrawn: number;
numCoinsTotal: number;
}
/**
* Fields that are present in every pending operation.
*/
export interface PendingOperationInfoCommon {
/**
* Type of the pending operation.
*/
type: PendingOperationType;
/**
* Set to true if the operation indicates that something is really in progress,
* as opposed to some regular scheduled operation or a permanent failure.
*/
givesLifeness: boolean;
/**
* Retry info, not available on all pending operations.
* If it is available, it must have the same name.
*/
retryInfo?: RetryInfo;
}
/**
* Response returned from the pending operations API.
*/
export interface PendingOperationsResponse {
/**
* List of pending operations.
*/
pendingOperations: PendingOperationInfo[];
/**
* Current wallet balance, including pending balances.
*/
walletBalance: BalancesResponse;
/**
* When is the next pending operation due to be re-tried?
*/
nextRetryDelay: Duration;
/**
* Does this response only include pending operations that
* are due to be executed right now?
*/
onlyDue: boolean;
}

View File

@ -1,337 +0,0 @@
/*
This file is part of GNU Taler
(C) 2019 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Type and schema definitions for the wallet's transaction list.
*
* @author Florian Dold
* @author Torsten Grote
*/
/**
* Imports.
*/
import { Timestamp } from "../util/time";
import {
AmountString,
Product,
InternationalizedString,
MerchantInfo,
codecForInternationalizedString,
codecForMerchantInfo,
codecForProduct,
} from "./talerTypes";
import {
Codec,
buildCodecForObject,
codecOptional,
codecForString,
codecForList,
codecForAny,
} from "../util/codec";
import { TalerErrorDetails } from "./walletTypes";
export interface TransactionsRequest {
/**
* return only transactions in the given currency
*/
currency?: string;
/**
* if present, results will be limited to transactions related to the given search string
*/
search?: string;
}
export interface TransactionsResponse {
// a list of past and pending transactions sorted by pending, timestamp and transactionId.
// In case two events are both pending and have the same timestamp,
// they are sorted by the transactionId
// (lexically ascending and locale-independent comparison).
transactions: Transaction[];
}
export interface TransactionCommon {
// opaque unique ID for the transaction, used as a starting point for paginating queries
// and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
transactionId: string;
// the type of the transaction; different types might provide additional information
type: TransactionType;
// main timestamp of the transaction
timestamp: Timestamp;
// true if the transaction is still pending, false otherwise
// If a transaction is not longer pending, its timestamp will be updated,
// but its transactionId will remain unchanged
pending: boolean;
// Raw amount of the transaction (exclusive of fees or other extra costs)
amountRaw: AmountString;
// Amount added or removed from the wallet's balance (including all fees and other costs)
amountEffective: AmountString;
error?: TalerErrorDetails;
}
export type Transaction =
| TransactionWithdrawal
| TransactionPayment
| TransactionRefund
| TransactionTip
| TransactionRefresh;
export enum TransactionType {
Withdrawal = "withdrawal",
Payment = "payment",
Refund = "refund",
Refresh = "refresh",
Tip = "tip",
}
export enum WithdrawalType {
TalerBankIntegrationApi = "taler-bank-integration-api",
ManualTransfer = "manual-transfer",
}
export type WithdrawalDetails =
| WithdrawalDetailsForManualTransfer
| WithdrawalDetailsForTalerBankIntegrationApi;
interface WithdrawalDetailsForManualTransfer {
type: WithdrawalType.ManualTransfer;
/**
* Payto URIs that the exchange supports.
*
* Already contains the amount and message.
*/
exchangePaytoUris: string[];
}
interface WithdrawalDetailsForTalerBankIntegrationApi {
type: WithdrawalType.TalerBankIntegrationApi;
/**
* Set to true if the bank has confirmed the withdrawal, false if not.
* An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
* See also bankConfirmationUrl below.
*/
confirmed: boolean;
/**
* If the withdrawal is unconfirmed, this can include a URL for user
* initiated confirmation.
*/
bankConfirmationUrl?: string;
}
// This should only be used for actual withdrawals
// and not for tips that have their own transactions type.
interface TransactionWithdrawal extends TransactionCommon {
type: TransactionType.Withdrawal;
/**
* Exchange of the withdrawal.
*/
exchangeBaseUrl: string;
/**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
*/
amountEffective: AmountString;
withdrawalDetails: WithdrawalDetails;
}
export enum PaymentStatus {
/**
* Explicitly aborted after timeout / failure
*/
Aborted = "aborted",
/**
* Payment failed, wallet will auto-retry.
* User should be given the option to retry now / abort.
*/
Failed = "failed",
/**
* Paid successfully
*/
Paid = "paid",
/**
* User accepted, payment is processing.
*/
Accepted = "accepted",
}
export interface TransactionPayment extends TransactionCommon {
type: TransactionType.Payment;
/**
* Additional information about the payment.
*/
info: OrderShortInfo;
/**
* Wallet-internal end-to-end identifier for the payment.
*/
proposalId: string;
/**
* How far did the wallet get with processing the payment?
*/
status: PaymentStatus;
/**
* Amount that must be paid for the contract
*/
amountRaw: AmountString;
/**
* Amount that was paid, including deposit, wire and refresh fees.
*/
amountEffective: AmountString;
}
export interface OrderShortInfo {
/**
* Order ID, uniquely identifies the order within a merchant instance
*/
orderId: string;
/**
* Hash of the contract terms.
*/
contractTermsHash: string;
/**
* More information about the merchant
*/
merchant: MerchantInfo;
/**
* Summary of the order, given by the merchant
*/
summary: string;
/**
* Map from IETF BCP 47 language tags to localized summaries
*/
summary_i18n?: InternationalizedString;
/**
* List of products that are part of the order
*/
products: Product[] | undefined;
/**
* URL of the fulfillment, given by the merchant
*/
fulfillmentUrl?: string;
/**
* Plain text message that should be shown to the user
* when the payment is complete.
*/
fulfillmentMessage?: string;
/**
* Translations of fulfillmentMessage.
*/
fulfillmentMessage_i18n?: InternationalizedString;
}
interface TransactionRefund extends TransactionCommon {
type: TransactionType.Refund;
// ID for the transaction that is refunded
refundedTransactionId: string;
// Additional information about the refunded payment
info: OrderShortInfo;
// Amount that has been refunded by the merchant
amountRaw: AmountString;
// Amount will be added to the wallet's balance after fees and refreshing
amountEffective: AmountString;
}
interface TransactionTip extends TransactionCommon {
type: TransactionType.Tip;
// Raw amount of the tip, without extra fees that apply
amountRaw: AmountString;
// Amount will be (or was) added to the wallet's balance after fees and refreshing
amountEffective: AmountString;
merchantBaseUrl: string;
}
// A transaction shown for refreshes that are not associated to other transactions
// such as a refresh necessary before coin expiration.
// It should only be returned by the API if the effective amount is different from zero.
interface TransactionRefresh extends TransactionCommon {
type: TransactionType.Refresh;
// Exchange that the coins are refreshed with
exchangeBaseUrl: string;
// Raw amount that is refreshed
amountRaw: AmountString;
// Amount that will be paid as fees for the refresh
amountEffective: AmountString;
}
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
buildCodecForObject<TransactionsRequest>()
.property("currency", codecOptional(codecForString()))
.property("search", codecOptional(codecForString()))
.build("TransactionsRequest");
// FIXME: do full validation here!
export const codecForTransactionsResponse = (): Codec<TransactionsResponse> =>
buildCodecForObject<TransactionsResponse>()
.property("transactions", codecForList(codecForAny()))
.build("TransactionsResponse");
export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
buildCodecForObject<OrderShortInfo>()
.property("contractTermsHash", codecForString())
.property("fulfillmentMessage", codecOptional(codecForString()))
.property(
"fulfillmentMessage_i18n",
codecOptional(codecForInternationalizedString()),
)
.property("fulfillmentUrl", codecOptional(codecForString()))
.property("merchant", codecForMerchantInfo())
.property("orderId", codecForString())
.property("products", codecOptional(codecForList(codecForProduct())))
.property("summary", codecForString())
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
.build("OrderShortInfo");

View File

@ -55,7 +55,7 @@ import {
codecForContractTerms,
ContractTerms,
} from "./talerTypes";
import { OrderShortInfo, codecForOrderShortInfo } from "./transactions";
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes";
/**
* Response for the create reserve request to the wallet.

View File

@ -583,7 +583,7 @@ export class Database {
}
async getIndexed<Ind extends Index<string, string, any, any>>(
index: InferIndex<Ind>,
index: Ind,
key: IDBValidKey,
): Promise<IndexRecord<Ind> | undefined> {
const tx = this.db.transaction([index.storeName], "readonly");

View File

@ -1,285 +0,0 @@
/*
This file is part of GNU Taler
(C) 2020 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import test from "ava";
import {
reconcileReserveHistory,
summarizeReserveHistory,
} from "./reserveHistoryUtil";
import {
WalletReserveHistoryItem,
WalletReserveHistoryItemType,
} from "../types/dbTypes";
import {
ReserveTransaction,
ReserveTransactionType,
} from "../types/ReserveTransaction";
import { Amounts } from "./amounts";
test("basics", (t) => {
const r = reconcileReserveHistory([], []);
t.deepEqual(r.updatedLocalHistory, []);
});
test("unmatched credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 1);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
});
test("unmatched credit #2", (t) => {
const localHistory: WalletReserveHistoryItem[] = [];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:50",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC02",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
});
test("matched credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
matchedExchangeTransaction: {
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:50",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC02",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
});
test("fulfilling credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:50",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC02",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
});
test("unfulfilled credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:50",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC02",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
});
test("awaited credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:50"),
},
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:50");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
});
test("withdrawal new match", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
matchedExchangeTransaction: {
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
},
{
type: WalletReserveHistoryItemType.Withdraw,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Withdraw,
amount: "TESTKUDOS:5",
h_coin_envelope: "foobar",
h_denom_pub: "foobar",
reserve_sig: "foobar",
withdraw_fee: "TESTKUDOS:0.1",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:95");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
});
test("claimed but now arrived", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
matchedExchangeTransaction: {
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
},
{
type: WalletReserveHistoryItemType.Withdraw,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
});

View File

@ -1,363 +0,0 @@
/*
This file is part of GNU Taler
(C) 2020 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Helpers for dealing with reserve histories.
*
* @author Florian Dold <dold@taler.net>
*/
/**
* Imports.
*/
import {
WalletReserveHistoryItem,
WalletReserveHistoryItemType,
} from "../types/dbTypes";
import {
ReserveTransaction,
ReserveTransactionType,
} from "../types/ReserveTransaction";
import * as Amounts from "../util/amounts";
import { timestampCmp } from "./time";
import { deepCopy } from "./helpers";
import { AmountJson } from "../util/amounts";
/**
* Result of a reserve reconciliation.
*/
export interface ReserveReconciliationResult {
/**
* The wallet's local history reconciled with the exchange's reserve history.
*/
updatedLocalHistory: WalletReserveHistoryItem[];
/**
* History items that were newly created, subset of the
* updatedLocalHistory items.
*/
newAddedItems: WalletReserveHistoryItem[];
/**
* History items that were newly matched, subset of the
* updatedLocalHistory items.
*/
newMatchedItems: WalletReserveHistoryItem[];
}
/**
* Various totals computed from the wallet's view
* on the reserve history.
*/
export interface ReserveHistorySummary {
/**
* Balance computed by the wallet, should match the balance
* computed by the reserve.
*/
computedReserveBalance: Amounts.AmountJson;
/**
* Reserve balance that is still available for withdrawal.
*/
unclaimedReserveAmount: Amounts.AmountJson;
/**
* Amount that we're still expecting to come into the reserve.
*/
awaitedReserveAmount: Amounts.AmountJson;
/**
* Amount withdrawn from the reserve so far. Only counts
* finished withdrawals, not withdrawals in progress.
*/
withdrawnAmount: Amounts.AmountJson;
}
/**
* Check if two reserve history items (exchange's version) match.
*/
function isRemoteHistoryMatch(
t1: ReserveTransaction,
t2: ReserveTransaction,
): boolean {
switch (t1.type) {
case ReserveTransactionType.Closing: {
return t1.type === t2.type && t1.wtid == t2.wtid;
}
case ReserveTransactionType.Credit: {
return t1.type === t2.type && t1.wire_reference === t2.wire_reference;
}
case ReserveTransactionType.Recoup: {
return (
t1.type === t2.type &&
t1.coin_pub === t2.coin_pub &&
timestampCmp(t1.timestamp, t2.timestamp) === 0
);
}
case ReserveTransactionType.Withdraw: {
return t1.type === t2.type && t1.h_coin_envelope === t2.h_coin_envelope;
}
}
}
/**
* Check a local reserve history item and a remote history item are a match.
*/
export function isLocalRemoteHistoryMatch(
t1: WalletReserveHistoryItem,
t2: ReserveTransaction,
): boolean {
switch (t1.type) {
case WalletReserveHistoryItemType.Credit: {
return (
t2.type === ReserveTransactionType.Credit &&
!!t1.expectedAmount &&
Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
);
}
case WalletReserveHistoryItemType.Withdraw:
return (
t2.type === ReserveTransactionType.Withdraw &&
!!t1.expectedAmount &&
Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
);
case WalletReserveHistoryItemType.Recoup: {
return (
t2.type === ReserveTransactionType.Recoup &&
!!t1.expectedAmount &&
Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
);
}
}
return false;
}
/**
* Compute totals for the wallet's view of the reserve history.
*/
export function summarizeReserveHistory(
localHistory: WalletReserveHistoryItem[],
currency: string,
): ReserveHistorySummary {
const posAmounts: AmountJson[] = [];
const negAmounts: AmountJson[] = [];
const expectedPosAmounts: AmountJson[] = [];
const expectedNegAmounts: AmountJson[] = [];
const withdrawnAmounts: AmountJson[] = [];
for (const item of localHistory) {
switch (item.type) {
case WalletReserveHistoryItemType.Credit:
if (item.matchedExchangeTransaction) {
posAmounts.push(
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
);
} else if (item.expectedAmount) {
expectedPosAmounts.push(item.expectedAmount);
}
break;
case WalletReserveHistoryItemType.Recoup:
if (item.matchedExchangeTransaction) {
if (item.matchedExchangeTransaction) {
posAmounts.push(
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
);
} else if (item.expectedAmount) {
expectedPosAmounts.push(item.expectedAmount);
} else {
throw Error("invariant failed");
}
}
break;
case WalletReserveHistoryItemType.Closing:
if (item.matchedExchangeTransaction) {
negAmounts.push(
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
);
} else {
throw Error("invariant failed");
}
break;
case WalletReserveHistoryItemType.Withdraw:
if (item.matchedExchangeTransaction) {
negAmounts.push(
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
);
withdrawnAmounts.push(
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
);
} else if (item.expectedAmount) {
expectedNegAmounts.push(item.expectedAmount);
} else {
throw Error("invariant failed");
}
break;
}
}
const z = Amounts.getZero(currency);
const computedBalance = Amounts.sub(
Amounts.add(z, ...posAmounts).amount,
...negAmounts,
).amount;
const unclaimedReserveAmount = Amounts.sub(
Amounts.add(z, ...posAmounts).amount,
...negAmounts,
...expectedNegAmounts,
).amount;
const awaitedReserveAmount = Amounts.sub(
Amounts.add(z, ...expectedPosAmounts).amount,
...expectedNegAmounts,
).amount;
const withdrawnAmount = Amounts.add(z, ...withdrawnAmounts).amount;
return {
computedReserveBalance: computedBalance,
unclaimedReserveAmount: unclaimedReserveAmount,
awaitedReserveAmount: awaitedReserveAmount,
withdrawnAmount,
};
}
/**
* Reconcile the wallet's local model of the reserve history
* with the reserve history of the exchange.
*/
export function reconcileReserveHistory(
localHistory: WalletReserveHistoryItem[],
remoteHistory: ReserveTransaction[],
): ReserveReconciliationResult {
const updatedLocalHistory: WalletReserveHistoryItem[] = deepCopy(
localHistory,
);
const newMatchedItems: WalletReserveHistoryItem[] = [];
const newAddedItems: WalletReserveHistoryItem[] = [];
const remoteMatched = remoteHistory.map(() => false);
const localMatched = localHistory.map(() => false);
// Take care of deposits
// First, see which pairs are already a definite match.
for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
const rhi = remoteHistory[remoteIndex];
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
if (localMatched[localIndex]) {
continue;
}
const lhi = localHistory[localIndex];
if (!lhi.matchedExchangeTransaction) {
continue;
}
if (isRemoteHistoryMatch(rhi, lhi.matchedExchangeTransaction)) {
localMatched[localIndex] = true;
remoteMatched[remoteIndex] = true;
break;
}
}
}
// Check that all previously matched items are still matched
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
if (localMatched[localIndex]) {
continue;
}
const lhi = localHistory[localIndex];
if (lhi.matchedExchangeTransaction) {
// Don't use for further matching
localMatched[localIndex] = true;
// FIXME: emit some error here!
throw Error("previously matched reserve history item now unmatched");
}
}
// Next, find out if there are any exact new matches between local and remote
// history items
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
if (localMatched[localIndex]) {
continue;
}
const lhi = localHistory[localIndex];
for (
let remoteIndex = 0;
remoteIndex < remoteHistory.length;
remoteIndex++
) {
const rhi = remoteHistory[remoteIndex];
if (remoteMatched[remoteIndex]) {
continue;
}
if (isLocalRemoteHistoryMatch(lhi, rhi)) {
localMatched[localIndex] = true;
remoteMatched[remoteIndex] = true;
updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
newMatchedItems.push(lhi);
break;
}
}
}
// Finally we add new history items
for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
if (remoteMatched[remoteIndex]) {
continue;
}
const rhi = remoteHistory[remoteIndex];
let newItem: WalletReserveHistoryItem;
switch (rhi.type) {
case ReserveTransactionType.Closing: {
newItem = {
type: WalletReserveHistoryItemType.Closing,
matchedExchangeTransaction: rhi,
};
break;
}
case ReserveTransactionType.Credit: {
newItem = {
type: WalletReserveHistoryItemType.Credit,
matchedExchangeTransaction: rhi,
};
break;
}
case ReserveTransactionType.Recoup: {
newItem = {
type: WalletReserveHistoryItemType.Recoup,
matchedExchangeTransaction: rhi,
};
break;
}
case ReserveTransactionType.Withdraw: {
newItem = {
type: WalletReserveHistoryItemType.Withdraw,
matchedExchangeTransaction: rhi,
};
break;
}
}
updatedLocalHistory.push(newItem);
newAddedItems.push(newItem);
}
return {
updatedLocalHistory,
newAddedItems,
newMatchedItems,
};
}

View File

@ -130,7 +130,7 @@ import {
PendingOperationInfo,
PendingOperationsResponse,
PendingOperationType,
} from "./types/pending";
} from "./types/pendingTypes";
import { WalletNotification, NotificationType } from "./types/notifications";
import {
processPurchaseQueryRefund,
@ -148,7 +148,7 @@ import {
TransactionsRequest,
TransactionsResponse,
codecForTransactionsRequest,
} from "./types/transactions";
} from "./types/transactionsTypes";
import { getTransactions } from "./operations/transactions";
import {
withdrawTestBalance,
@ -326,7 +326,7 @@ export class Wallet {
} = {},
): Promise<void> {
let done = false;
const p = new Promise((resolve, reject) => {
const p = new Promise<void>((resolve, reject) => {
// Monitor for conditions that means we're done or we
// should quit with an error (due to exceeded retries).
this.addNotificationListener((n) => {