wallet-core: use numeric status field to allow range queries

This commit is contained in:
Florian Dold 2022-09-21 20:46:45 +02:00
parent 5d31803c92
commit 7d6bcd42ea
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
10 changed files with 86 additions and 81 deletions

View File

@ -62,6 +62,8 @@ import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
* will have an index.
* - Amounts are stored as strings, except when they are needed for
* indexing.
* - Every record that has a corresponding transaction item must have
* an index for a mandatory timestamp field.
* - Optional fields should be avoided, use "T | undefined" instead.
*
* @author Florian Dold <dold@taler.net>
@ -94,38 +96,45 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
*/
export const WALLET_DB_MINOR_VERSION = 1;
export namespace OperationStatusRange {
export const ACTIVE_START = 10;
export const ACTIVE_END = 29;
export const DORMANT_START = 40;
export const DORMANT_END = 59;
}
/**
* Status of a withdrawal.
*/
export enum ReserveRecordStatus {
export enum WithdrawalGroupStatus {
/**
* Reserve must be registered with the bank.
*/
RegisteringBank = "registering-bank",
RegisteringBank = OperationStatusRange.ACTIVE_START,
/**
* We've registered reserve's information with the bank
* and are now waiting for the user to confirm the withdraw
* with the bank (typically 2nd factor auth).
*/
WaitConfirmBank = "wait-confirm-bank",
WaitConfirmBank = OperationStatusRange.ACTIVE_START + 1,
/**
* Querying reserve status with the exchange.
*/
QueryingStatus = "querying-status",
QueryingStatus = OperationStatusRange.ACTIVE_START + 2,
/**
* The corresponding withdraw record has been created.
* No further processing is done, unless explicitly requested
* by the user.
*/
Dormant = "dormant",
Finished = OperationStatusRange.DORMANT_START,
/**
* The bank aborted the withdrawal.
*/
BankAborted = "bank-aborted",
BankAborted = OperationStatusRange.DORMANT_START + 1,
}
/**
@ -1354,20 +1363,12 @@ export interface WithdrawalGroupRecord {
*/
timestampFinish?: TalerProtocolTimestamp;
/**
* Operation status of the withdrawal group.
* Used for indexing in the database.
*
* FIXME: Redundant with reserveStatus
*/
operationStatus: OperationStatus;
/**
* Current status of the reserve.
*
* FIXME: Wrong name!
*/
reserveStatus: ReserveRecordStatus;
status: WithdrawalGroupStatus;
/**
* Amount that was sent by the user to fund the reserve.
@ -1947,7 +1948,7 @@ export const WalletStoresV1 = {
}),
{
byReservePub: describeIndex("byReservePub", "reservePub"),
byStatus: describeIndex("byStatus", "operationStatus"),
byStatus: describeIndex("byStatus", "status"),
byTalerWithdrawUri: describeIndex(
"byTalerWithdrawUri",
"wgInfo.bankInfo.talerWithdrawUri",

View File

@ -71,6 +71,7 @@ import {
RefreshCoinStatus,
RefundState,
WALLET_BACKUP_STATE_KEY,
WithdrawalGroupStatus,
WithdrawalRecordType,
} from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js";
@ -167,8 +168,9 @@ export async function exportBackup(
instructed_amount: Amounts.stringify(wg.instructedAmount),
reserve_priv: wg.reservePriv,
restrict_age: wg.restrictAge,
// FIXME: proper status conversion!
operation_status:
wg.operationStatus == OperationStatus.Finished
wg.status == WithdrawalGroupStatus.Finished
? BackupOperationStatus.Finished
: BackupOperationStatus.Pending,
selected_denoms_uid: wg.denomSelUid,

View File

@ -52,7 +52,7 @@ import {
RefreshSessionRecord,
RefundState,
ReserveBankInfo,
ReserveRecordStatus,
WithdrawalGroupStatus,
WalletContractData,
WalletRefundItem,
WalletStoresV1,
@ -531,9 +531,6 @@ export async function importBackup(
exchangeBaseUrl: backupWg.exchange_base_url,
instructedAmount: Amounts.parseOrThrow(backupWg.instructed_amount),
secretSeed: backupWg.secret_seed,
operationStatus: backupWg.timestamp_finish
? OperationStatus.Finished
: OperationStatus.Pending,
denomsSel: await getDenomSelStateFromBackup(
tx,
backupWg.exchange_base_url,
@ -545,9 +542,9 @@ export async function importBackup(
),
reservePriv: backupWg.reserve_priv,
reservePub,
reserveStatus: backupWg.timestamp_finish
? ReserveRecordStatus.Dormant
: ReserveRecordStatus.QueryingStatus, // FIXME!
status: backupWg.timestamp_finish
? WithdrawalGroupStatus.Finished
: WithdrawalGroupStatus.QueryingStatus, // FIXME!
timestampStart: backupWg.timestamp_created,
wgInfo,
restrictAge: backupWg.restrict_age,

View File

@ -65,7 +65,7 @@ import {
import {
CoinStatus,
MergeReserveInfo,
ReserveRecordStatus,
WithdrawalGroupStatus,
WalletStoresV1,
WithdrawalRecordType,
} from "../db.js";
@ -544,7 +544,7 @@ export async function acceptPeerPushPayment(
contractTerms: peerInc.contractTerms,
},
exchangeBaseUrl: peerInc.exchangeBaseUrl,
reserveStatus: ReserveRecordStatus.QueryingStatus,
reserveStatus: WithdrawalGroupStatus.QueryingStatus,
reserveKeyPair: {
priv: mergeReserveInfo.reservePriv,
pub: mergeReserveInfo.reservePub,
@ -828,7 +828,7 @@ export async function initiatePeerRequestForPay(
contractPriv: econtractResp.contractPriv,
},
exchangeBaseUrl: req.exchangeBaseUrl,
reserveStatus: ReserveRecordStatus.QueryingStatus,
reserveStatus: WithdrawalGroupStatus.QueryingStatus,
reserveKeyPair: {
priv: mergeReserveInfo.reservePriv,
pub: mergeReserveInfo.reservePub,

View File

@ -28,6 +28,9 @@ import {
BackupProviderStateTag,
RefreshCoinStatus,
OperationStatus,
WithdrawalGroupRecord,
WithdrawalGroupStatus,
OperationStatusRange,
} from "../db.js";
import {
PendingOperationsResponse,
@ -38,6 +41,7 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { GetReadOnlyAccess } from "../util/query.js";
import { RetryTags } from "../util/retries.js";
import { Wallet } from "../wallet.js";
import { GlobalIDB } from "@gnu-taler/idb-bridge";
async function gatherExchangePending(
tx: GetReadOnlyAccess<{
@ -120,7 +124,10 @@ async function gatherWithdrawalPending(
resp: PendingOperationsResponse,
): Promise<void> {
const wsrs = await tx.withdrawalGroups.indexes.byStatus.getAll(
OperationStatus.Pending,
GlobalIDB.KeyRange.bound(
OperationStatusRange.ACTIVE_START,
OperationStatusRange.ACTIVE_END,
),
);
for (const wsr of wsrs) {
if (wsr.timestampFinish) {

View File

@ -44,7 +44,7 @@ import {
CoinStatus,
RecoupGroupRecord,
RefreshCoinSource,
ReserveRecordStatus,
WithdrawalGroupStatus,
WalletStoresV1,
WithdrawalRecordType,
WithdrawCoinSource,
@ -382,7 +382,7 @@ export async function processRecoupGroupHandler(
await internalCreateWithdrawalGroup(ws, {
amount: Amounts.parseOrThrow(result.balance),
exchangeBaseUrl: recoupGroup.exchangeBaseUrl,
reserveStatus: ReserveRecordStatus.QueryingStatus,
reserveStatus: WithdrawalGroupStatus.QueryingStatus,
reserveKeyPair: {
pub: reservePub,
priv: reservePrivMap[reservePub],

View File

@ -31,11 +31,9 @@ import {
TalerProtocolTimestamp,
Transaction,
TransactionByIdRequest,
TransactionRefund,
TransactionsRequest,
TransactionsResponse,
TransactionType,
WithdrawalDetails,
WithdrawalType,
} from "@gnu-taler/taler-util";
import {

View File

@ -71,7 +71,7 @@ import {
ExchangeRecord,
OperationStatus,
PlanchetRecord,
ReserveRecordStatus,
WithdrawalGroupStatus,
WalletStoresV1,
WgInfo,
WithdrawalGroupRecord,
@ -91,7 +91,11 @@ import {
readSuccessResponseJsonOrThrow,
throwUnexpectedRequestError,
} from "../util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import {
checkDbInvariant,
checkLogicInvariant,
InvariantViolatedError,
} from "../util/invariants.js";
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
import {
OperationAttemptResult,
@ -962,7 +966,7 @@ async function queryReserve(
withdrawalGroupId,
});
checkDbInvariant(!!withdrawalGroup);
if (withdrawalGroup.reserveStatus !== ReserveRecordStatus.QueryingStatus) {
if (withdrawalGroup.status !== WithdrawalGroupStatus.QueryingStatus) {
return { ready: true };
}
const reservePub = withdrawalGroup.reservePub;
@ -1010,7 +1014,7 @@ async function queryReserve(
logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
return;
}
wg.reserveStatus = ReserveRecordStatus.Dormant;
wg.status = WithdrawalGroupStatus.Finished;
await tx.withdrawalGroups.put(wg);
});
@ -1039,13 +1043,13 @@ export async function processWithdrawalGroup(
throw Error(`withdrawal group ${withdrawalGroupId} not found`);
}
switch (withdrawalGroup.reserveStatus) {
case ReserveRecordStatus.RegisteringBank:
switch (withdrawalGroup.status) {
case WithdrawalGroupStatus.RegisteringBank:
await processReserveBankStatus(ws, withdrawalGroupId);
return await processWithdrawalGroup(ws, withdrawalGroupId, {
forceNow: true,
});
case ReserveRecordStatus.QueryingStatus: {
case WithdrawalGroupStatus.QueryingStatus: {
const res = await queryReserve(ws, withdrawalGroupId);
if (res.ready) {
return await processWithdrawalGroup(ws, withdrawalGroupId, {
@ -1057,7 +1061,7 @@ export async function processWithdrawalGroup(
result: undefined,
};
}
case ReserveRecordStatus.WaitConfirmBank: {
case WithdrawalGroupStatus.WaitConfirmBank: {
const res = await processReserveBankStatus(ws, withdrawalGroupId);
switch (res.status) {
case BankStatusResultCode.Aborted:
@ -1075,23 +1079,20 @@ export async function processWithdrawalGroup(
}
break;
}
case ReserveRecordStatus.BankAborted: {
case WithdrawalGroupStatus.BankAborted: {
// FIXME
return {
type: OperationAttemptResultType.Pending,
result: undefined,
};
}
case ReserveRecordStatus.Dormant:
case WithdrawalGroupStatus.Finished:
// We can try to withdraw, nothing needs to be done with the reserve.
break;
default:
logger.warn(
"unknown reserve record status:",
withdrawalGroup.reserveStatus,
throw new InvariantViolatedError(
`unknown reserve record status: ${withdrawalGroup.status}`,
);
assertUnreachable(withdrawalGroup.reserveStatus);
break;
}
await ws.exchangeOps.updateExchangeFromUrl(
@ -1108,7 +1109,7 @@ export async function processWithdrawalGroup(
if (!wg) {
return;
}
wg.operationStatus = OperationStatus.Finished;
wg.status = WithdrawalGroupStatus.Finished;
wg.timestampFinish = TalerProtocolTimestamp.now();
await tx.withdrawalGroups.put(wg);
});
@ -1192,7 +1193,7 @@ export async function processWithdrawalGroup(
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
finishedForFirstTime = true;
wg.timestampFinish = TalerProtocolTimestamp.now();
wg.operationStatus = OperationStatus.Finished;
wg.status = WithdrawalGroupStatus.Finished;
}
await tx.withdrawalGroups.put(wg);
@ -1508,9 +1509,9 @@ async function registerReserveWithBank(
.runReadOnly(async (tx) => {
return await tx.withdrawalGroups.get(withdrawalGroupId);
});
switch (withdrawalGroup?.reserveStatus) {
case ReserveRecordStatus.WaitConfirmBank:
case ReserveRecordStatus.RegisteringBank:
switch (withdrawalGroup?.status) {
case WithdrawalGroupStatus.WaitConfirmBank:
case WithdrawalGroupStatus.RegisteringBank:
break;
default:
return;
@ -1544,9 +1545,9 @@ async function registerReserveWithBank(
if (!r) {
return;
}
switch (r.reserveStatus) {
case ReserveRecordStatus.RegisteringBank:
case ReserveRecordStatus.WaitConfirmBank:
switch (r.status) {
case WithdrawalGroupStatus.RegisteringBank:
case WithdrawalGroupStatus.WaitConfirmBank:
break;
default:
return;
@ -1557,8 +1558,7 @@ async function registerReserveWithBank(
r.wgInfo.bankInfo.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
AbsoluteTime.now(),
);
r.reserveStatus = ReserveRecordStatus.WaitConfirmBank;
r.operationStatus = OperationStatus.Pending;
r.status = WithdrawalGroupStatus.WaitConfirmBank;
await tx.withdrawalGroups.put(r);
});
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
@ -1575,9 +1575,9 @@ async function processReserveBankStatus(
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId,
});
switch (withdrawalGroup?.reserveStatus) {
case ReserveRecordStatus.WaitConfirmBank:
case ReserveRecordStatus.RegisteringBank:
switch (withdrawalGroup?.status) {
case WithdrawalGroupStatus.WaitConfirmBank:
case WithdrawalGroupStatus.RegisteringBank:
break;
default:
return {
@ -1616,9 +1616,9 @@ async function processReserveBankStatus(
if (!r) {
return;
}
switch (r.reserveStatus) {
case ReserveRecordStatus.RegisteringBank:
case ReserveRecordStatus.WaitConfirmBank:
switch (r.status) {
case WithdrawalGroupStatus.RegisteringBank:
case WithdrawalGroupStatus.WaitConfirmBank:
break;
default:
return;
@ -1628,8 +1628,7 @@ async function processReserveBankStatus(
}
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
r.wgInfo.bankInfo.timestampBankConfirmed = now;
r.reserveStatus = ReserveRecordStatus.BankAborted;
r.operationStatus = OperationStatus.Finished;
r.status = WithdrawalGroupStatus.BankAborted;
await tx.withdrawalGroups.put(r);
});
return {
@ -1644,7 +1643,7 @@ async function processReserveBankStatus(
}
// FIXME: Why do we do this?!
if (withdrawalGroup.reserveStatus === ReserveRecordStatus.RegisteringBank) {
if (withdrawalGroup.status === WithdrawalGroupStatus.RegisteringBank) {
await registerReserveWithBank(ws, withdrawalGroupId);
return await processReserveBankStatus(ws, withdrawalGroupId);
}
@ -1657,9 +1656,9 @@ async function processReserveBankStatus(
return;
}
// Re-check reserve status within transaction
switch (r.reserveStatus) {
case ReserveRecordStatus.RegisteringBank:
case ReserveRecordStatus.WaitConfirmBank:
switch (r.status) {
case WithdrawalGroupStatus.RegisteringBank:
case WithdrawalGroupStatus.WaitConfirmBank:
break;
default:
return;
@ -1671,8 +1670,7 @@ async function processReserveBankStatus(
logger.info("withdrawal: transfer confirmed by bank.");
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
r.wgInfo.bankInfo.timestampBankConfirmed = now;
r.reserveStatus = ReserveRecordStatus.QueryingStatus;
r.operationStatus = OperationStatus.Pending;
r.status = WithdrawalGroupStatus.QueryingStatus;
} else {
logger.info("withdrawal: transfer not yet confirmed by bank");
r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
@ -1689,7 +1687,7 @@ async function processReserveBankStatus(
export async function internalCreateWithdrawalGroup(
ws: InternalWalletState,
args: {
reserveStatus: ReserveRecordStatus;
reserveStatus: WithdrawalGroupStatus;
amount: AmountJson;
exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
@ -1728,12 +1726,11 @@ export async function internalCreateWithdrawalGroup(
exchangeBaseUrl: canonExchange,
instructedAmount: amount,
timestampStart: now,
operationStatus: OperationStatus.Pending,
rawWithdrawalAmount: initialDenomSel.totalWithdrawCost,
secretSeed,
reservePriv: reserveKeyPair.priv,
reservePub: reserveKeyPair.pub,
reserveStatus: args.reserveStatus,
status: args.reserveStatus,
withdrawalGroupId,
restrictAge: args.restrictAge,
senderWire: undefined,
@ -1839,7 +1836,7 @@ export async function acceptWithdrawalFromUri(
},
restrictAge: req.restrictAge,
forcedDenomSel: req.forcedDenomSel,
reserveStatus: ReserveRecordStatus.RegisteringBank,
reserveStatus: WithdrawalGroupStatus.RegisteringBank,
});
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
@ -1850,9 +1847,7 @@ export async function acceptWithdrawalFromUri(
const processedWithdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId,
});
if (
processedWithdrawalGroup?.reserveStatus === ReserveRecordStatus.BankAborted
) {
if (processedWithdrawalGroup?.status === WithdrawalGroupStatus.BankAborted) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
{},
@ -1898,7 +1893,7 @@ export async function createManualWithdrawal(
exchangeBaseUrl: req.exchangeBaseUrl,
forcedDenomSel: req.forcedDenomSel,
restrictAge: req.restrictAge,
reserveStatus: ReserveRecordStatus.QueryingStatus,
reserveStatus: WithdrawalGroupStatus.QueryingStatus,
});
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;

View File

@ -14,6 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
export class InvariantViolatedError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, InvariantViolatedError.prototype);
}
}
/**
* Helpers for invariants.
*/

View File

@ -36,8 +36,6 @@ import {
IDBKeyRange,
} from "@gnu-taler/idb-bridge";
import { Logger } from "@gnu-taler/taler-util";
import { performanceNow } from "./timer.js";
import { access } from "fs";
const logger = new Logger("query.ts");