wallet-core: towards DD48

This commit is contained in:
Florian Dold 2023-08-30 15:54:56 +02:00
parent 88f7338d7c
commit d19aef746c
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
10 changed files with 182 additions and 93 deletions

View File

@ -312,6 +312,14 @@ export namespace Duration {
}
export namespace AbsoluteTime {
export function getStampMsNow(): number {
return new Date().getTime();
}
export function getStampMsNever(): number {
return Number.MAX_SAFE_INTEGER;
}
export function now(): AbsoluteTime {
return {
t_ms: new Date().getTime() + timeshift,
@ -398,6 +406,13 @@ export namespace AbsoluteTime {
};
}
export function fromStampMs(stampMs: number): AbsoluteTime {
return {
t_ms: stampMs,
[opaque_AbsoluteTime]: true,
};
}
export function fromPreciseTimestamp(t: TalerPreciseTimestamp): AbsoluteTime {
if (t.t_s === "never") {
return { t_ms: "never", [opaque_AbsoluteTime]: true };
@ -409,6 +424,13 @@ export namespace AbsoluteTime {
};
}
export function toStampMs(at: AbsoluteTime): number {
if (at.t_ms === "never") {
return Number.MAX_SAFE_INTEGER;
}
return at.t_ms;
}
export function toPreciseTimestamp(at: AbsoluteTime): TalerPreciseTimestamp {
if (at.t_ms == "never") {
return {

View File

@ -1262,17 +1262,25 @@ export interface ExchangeFullDetails {
}
export enum ExchangeTosStatus {
New = "new",
Pending = "pending",
Proposed = "proposed",
Accepted = "accepted",
Changed = "changed",
NotFound = "not-found",
Unknown = "unknown",
}
export enum ExchangeEntryStatus {
Unknown = "unknown",
Outdated = "outdated",
Ok = "ok",
Preset = "preset",
Ephemeral = "ephemeral",
Used = "used",
}
export enum ExchangeUpdateStatus {
Initial = "initial",
InitialUpdate = "initial(update)",
Suspended = "suspended",
Failed = "failed",
OutdatedUpdate = "outdated(update)",
Ready = "ready",
ReadyUpdate = "ready(update)",
}
export interface OperationErrorInfo {
@ -1285,13 +1293,9 @@ export interface ExchangeListItem {
currency: string | undefined;
paytoUris: string[];
tosStatus: ExchangeTosStatus;
exchangeStatus: ExchangeEntryStatus;
exchangeEntryStatus: ExchangeEntryStatus;
exchangeUpdateStatus: ExchangeUpdateStatus;
ageRestrictionOptions: number[];
/**
* Permanently added to the wallet, as opposed to just
* temporarily queried.
*/
permanent: boolean;
/**
* Information about the last error that occurred when trying
@ -1370,8 +1374,8 @@ export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
.property("exchangeBaseUrl", codecForString())
.property("paytoUris", codecForList(codecForString()))
.property("tosStatus", codecForAny())
.property("exchangeStatus", codecForAny())
.property("permanent", codecForBoolean())
.property("exchangeEntryStatus", codecForAny())
.property("exchangeUpdateStatus", codecForAny())
.property("ageRestrictionOptions", codecForList(codecForNumber()))
.build("ExchangeListItem");

View File

@ -42,7 +42,7 @@ import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import {
ExchangeDetailsRecord,
ExchangeRecord,
ExchangeEntryRecord,
RefreshReasonDetails,
WalletStoresV1,
} from "./db.js";
@ -108,7 +108,7 @@ export interface ExchangeOperations {
): Promise<ExchangeDetailsRecord | undefined>;
getExchangeTrust(
ws: InternalWalletState,
exchangeInfo: ExchangeRecord,
exchangeInfo: ExchangeEntryRecord,
): Promise<TrustInfo>;
updateExchangeFromUrl(
ws: InternalWalletState,
@ -118,7 +118,7 @@ export interface ExchangeOperations {
cancellationToken?: CancellationToken;
},
): Promise<{
exchange: ExchangeRecord;
exchange: ExchangeEntryRecord;
exchangeDetails: ExchangeDetailsRecord;
}>;
}

View File

@ -342,20 +342,19 @@ export async function importBackup(
if (existingExchange) {
continue;
}
await tx.exchanges.put({
baseUrl: backupExchange.base_url,
detailsPointer: {
currency: backupExchange.currency,
masterPublicKey: backupExchange.master_public_key,
updateClock: backupExchange.update_clock,
},
permanent: true,
lastUpdate: undefined,
nextUpdate: TalerPreciseTimestamp.now(),
nextRefreshCheck: TalerPreciseTimestamp.now(),
lastKeysEtag: undefined,
lastWireEtag: undefined,
});
// await tx.exchanges.put({
// baseUrl: backupExchange.base_url,
// detailsPointer: {
// currency: backupExchange.currency,
// masterPublicKey: backupExchange.master_public_key,
// updateClock: backupExchange.update_clock,
// },
// lastUpdate: undefined,
// nextUpdate: TalerPreciseTimestamp.now(),
// nextRefreshCheck: TalerPreciseTimestamp.now(),
// lastKeysEtag: undefined,
// lastWireEtag: undefined,
// });
}
for (const backupExchangeDetails of backupBlob.exchange_details) {

View File

@ -30,6 +30,7 @@ import {
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
ExchangeUpdateStatus,
getErrorDetailFromException,
j2s,
Logger,
@ -47,7 +48,7 @@ import {
WalletStoresV1,
CoinRecord,
ExchangeDetailsRecord,
ExchangeRecord,
ExchangeEntryRecord,
BackupProviderRecord,
DepositGroupRecord,
PeerPullPaymentIncomingRecord,
@ -59,6 +60,8 @@ import {
RefreshGroupRecord,
RewardRecord,
WithdrawalGroupRecord,
ExchangeEntryDbUpdateStatus,
ExchangeEntryDbRecordStatus,
} from "../db.js";
import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
@ -529,16 +532,16 @@ export function getExchangeTosStatus(
exchangeDetails: ExchangeDetailsRecord,
): ExchangeTosStatus {
if (!exchangeDetails.tosAccepted) {
return ExchangeTosStatus.New;
return ExchangeTosStatus.Proposed;
}
if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {
return ExchangeTosStatus.Accepted;
}
return ExchangeTosStatus.Changed;
return ExchangeTosStatus.Proposed;
}
export function makeExchangeListItem(
r: ExchangeRecord,
r: ExchangeEntryRecord,
exchangeDetails: ExchangeDetailsRecord | undefined,
lastError: TalerErrorDetail | undefined,
): ExchangeListItem {
@ -547,30 +550,57 @@ export function makeExchangeListItem(
error: lastError,
}
: undefined;
if (!exchangeDetails) {
return {
exchangeBaseUrl: r.baseUrl,
currency: undefined,
tosStatus: ExchangeTosStatus.Unknown,
paytoUris: [],
exchangeStatus: ExchangeEntryStatus.Unknown,
permanent: r.permanent,
ageRestrictionOptions: [],
lastUpdateErrorInfo,
};
let exchangeUpdateStatus: ExchangeUpdateStatus;
switch (r.updateStatus) {
case ExchangeEntryDbUpdateStatus.Failed:
exchangeUpdateStatus = ExchangeUpdateStatus.Failed;
break;
case ExchangeEntryDbUpdateStatus.Initial:
exchangeUpdateStatus = ExchangeUpdateStatus.Initial;
break;
case ExchangeEntryDbUpdateStatus.InitialUpdate:
exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate;
break;
case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate;
break;
case ExchangeEntryDbUpdateStatus.Ready:
exchangeUpdateStatus = ExchangeUpdateStatus.Ready;
break;
case ExchangeEntryDbUpdateStatus.ReadyUpdate:
exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate;
break;
case ExchangeEntryDbUpdateStatus.Suspended:
exchangeUpdateStatus = ExchangeUpdateStatus.Suspended;
break;
}
let exchangeStatus;
exchangeStatus = ExchangeEntryStatus.Ok;
let exchangeEntryStatus: ExchangeEntryStatus;
switch (r.entryStatus) {
case ExchangeEntryDbRecordStatus.Ephemeral:
exchangeEntryStatus = ExchangeEntryStatus.Ephemeral;
break;
case ExchangeEntryDbRecordStatus.Preset:
exchangeEntryStatus = ExchangeEntryStatus.Preset;
break;
case ExchangeEntryDbRecordStatus.Used:
exchangeEntryStatus = ExchangeEntryStatus.Used;
break;
}
return {
exchangeBaseUrl: r.baseUrl,
currency: exchangeDetails.currency,
tosStatus: getExchangeTosStatus(exchangeDetails),
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
exchangeStatus,
permanent: r.permanent,
ageRestrictionOptions: exchangeDetails.ageMask
currency: exchangeDetails?.currency,
exchangeUpdateStatus,
exchangeEntryStatus,
tosStatus: exchangeDetails
? getExchangeTosStatus(exchangeDetails)
: ExchangeTosStatus.Pending,
ageRestrictionOptions: exchangeDetails?.ageMask
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
: [],
paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [],
lastUpdateErrorInfo,
};
}
@ -892,13 +922,13 @@ export namespace TaskIdentifiers {
export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId {
return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
}
export function forExchangeUpdate(exch: ExchangeRecord): TaskId {
export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskId {
return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
}
export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
}
export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId {
return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
}
export function forTipPickup(tipRecord: RewardRecord): TaskId {

View File

@ -32,6 +32,7 @@ import {
encodeCrock,
ExchangeAuditor,
ExchangeDenomination,
ExchangeEntryStatus,
ExchangeGlobalFees,
ExchangeSignKeyJson,
ExchangeWireJson,
@ -66,10 +67,15 @@ import {
DenominationRecord,
DenominationVerificationStatus,
ExchangeDetailsRecord,
ExchangeRecord,
ExchangeEntryRecord,
WalletStoresV1,
} from "../db.js";
import { isWithdrawableDenom } from "../index.js";
import {
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
isWithdrawableDenom,
WalletDbReadWriteTransaction,
} from "../index.js";
import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
import {
@ -326,6 +332,26 @@ export async function downloadExchangeInfo(
};
}
export async function addPresetExchangeEntry(
tx: WalletDbReadWriteTransaction<"exchanges">,
exchangeBaseUrl: string,
): Promise<void> {
let exchange = await tx.exchanges.get(exchangeBaseUrl);
if (!exchange) {
const r: ExchangeEntryRecord = {
entryStatus: ExchangeEntryDbRecordStatus.Preset,
updateStatus: ExchangeEntryDbUpdateStatus.Initial,
baseUrl: exchangeBaseUrl,
detailsPointer: undefined,
lastUpdate: undefined,
lastKeysEtag: undefined,
nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(),
nextUpdateStampMs: AbsoluteTime.getStampMsNever(),
};
await tx.exchanges.put(r);
}
}
export async function provideExchangeRecordInTx(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
@ -335,20 +361,20 @@ export async function provideExchangeRecordInTx(
baseUrl: string,
now: AbsoluteTime,
): Promise<{
exchange: ExchangeRecord;
exchange: ExchangeEntryRecord;
exchangeDetails: ExchangeDetailsRecord | undefined;
}> {
let exchange = await tx.exchanges.get(baseUrl);
if (!exchange) {
const r: ExchangeRecord = {
permanent: true,
const r: ExchangeEntryRecord = {
entryStatus: ExchangeEntryDbRecordStatus.Ephemeral,
updateStatus: ExchangeEntryDbUpdateStatus.InitialUpdate,
baseUrl: baseUrl,
detailsPointer: undefined,
lastUpdate: undefined,
nextUpdate: AbsoluteTime.toPreciseTimestamp(now),
nextRefreshCheck: AbsoluteTime.toPreciseTimestamp(now),
nextUpdateStampMs: AbsoluteTime.getStampMsNever(),
nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(),
lastKeysEtag: undefined,
lastWireEtag: undefined,
};
await tx.exchanges.put(r);
exchange = r;
@ -534,6 +560,10 @@ export async function downloadTosFromAcceptedFormat(
);
}
/**
* FIXME: Split this into two parts: (a) triggering the exchange
* to be updated and (b) waiting for the update to finish.
*/
export async function updateExchangeFromUrl(
ws: InternalWalletState,
baseUrl: string,
@ -543,7 +573,7 @@ export async function updateExchangeFromUrl(
cancellationToken?: CancellationToken;
} = {},
): Promise<{
exchange: ExchangeRecord;
exchange: ExchangeEntryRecord;
exchangeDetails: ExchangeDetailsRecord;
}> {
const canonUrl = canonicalizeBaseUrl(baseUrl);
@ -613,7 +643,7 @@ export async function updateExchangeFromUrlHandler(
!forceNow &&
exchangeDetails !== undefined &&
!AbsoluteTime.isExpired(
AbsoluteTime.fromPreciseTimestamp(exchange.nextUpdate),
AbsoluteTime.fromStampMs(exchange.nextUpdateStampMs),
)
) {
logger.trace("using existing exchange info");
@ -755,11 +785,11 @@ export async function updateExchangeFromUrlHandler(
newDetails.rowId = existingDetails.rowId;
}
r.lastUpdate = TalerPreciseTimestamp.now();
r.nextUpdate = AbsoluteTime.toPreciseTimestamp(
r.nextUpdateStampMs = AbsoluteTime.toStampMs(
AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry),
);
// New denominations might be available.
r.nextRefreshCheck = TalerPreciseTimestamp.now();
r.nextRefreshCheckStampMs = AbsoluteTime.getStampMsNow();
if (detailsPointerChanged) {
r.detailsPointer = {
currency: newDetails.currency,
@ -948,7 +978,7 @@ export async function getExchangePaytoUri(
*/
export async function getExchangeTrust(
ws: InternalWalletState,
exchangeInfo: ExchangeRecord,
exchangeInfo: ExchangeEntryRecord,
): Promise<TrustInfo> {
let isTrusted = false;
let isAudited = false;

View File

@ -45,6 +45,7 @@ import {
PeerPushPaymentIncomingRecord,
RefundGroupRecord,
RefundGroupStatus,
ExchangeEntryDbUpdateStatus,
} from "../db.js";
import {
PendingOperationsResponse,
@ -81,19 +82,25 @@ async function gatherExchangePending(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
exchanges: typeof WalletStoresV1.exchanges;
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
// FIXME: We should do a range query here based on the update time.
// FIXME: We should do a range query here based on the update time
// and/or the entry state.
await tx.exchanges.iter().forEachAsync(async (exch) => {
switch (exch.updateStatus) {
case ExchangeEntryDbUpdateStatus.Initial:
case ExchangeEntryDbUpdateStatus.Suspended:
case ExchangeEntryDbUpdateStatus.Failed:
return;
}
const opTag = TaskIdentifiers.forExchangeUpdate(exch);
let opr = await tx.operationRetries.get(opTag);
const timestampDue =
opr?.retryInfo.nextRetry ??
AbsoluteTime.fromPreciseTimestamp(exch.nextUpdate);
AbsoluteTime.fromStampMs(exch.nextUpdateStampMs);
resp.pendingOperations.push({
type: PendingTaskType.ExchangeUpdate,
...getPendingCommon(ws, opTag, timestampDue),
@ -108,7 +115,7 @@ async function gatherExchangePending(
resp.pendingOperations.push({
type: PendingTaskType.ExchangeCheckRefresh,
...getPendingCommon(ws, opTag, timestampDue),
timestampDue: AbsoluteTime.fromPreciseTimestamp(exch.nextRefreshCheck),
timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs),
givesLifeness: false,
exchangeBaseUrl: exch.baseUrl,
});
@ -184,8 +191,9 @@ export async function iterRecordsForWithdrawal(
WithdrawalGroupStatus.PendingRegisteringBank,
WithdrawalGroupStatus.PendingAml,
);
withdrawalGroupRecords =
await tx.withdrawalGroups.indexes.byStatus.getAll(range);
withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll(
range,
);
} else {
withdrawalGroupRecords =
await tx.withdrawalGroups.indexes.byStatus.getAll();
@ -344,12 +352,8 @@ export async function iterRecordsForRefund(
f: (r: RefundGroupRecord) => Promise<void>,
): Promise<void> {
if (filter.onlyState === "nonfinal") {
const keyRange = GlobalIDB.KeyRange.only(
RefundGroupStatus.Pending
);
await tx.refundGroups.indexes.byStatus
.iter(keyRange)
.forEachAsync(f);
const keyRange = GlobalIDB.KeyRange.only(RefundGroupStatus.Pending);
await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
await tx.refundGroups.iter().forEachAsync(f);
}

View File

@ -1196,8 +1196,8 @@ export async function autoRefresh(
logger.trace(
`next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,
);
exchange.nextRefreshCheck =
AbsoluteTime.toPreciseTimestamp(minCheckThreshold);
exchange.nextRefreshCheckStampMs =
AbsoluteTime.toStampMs(minCheckThreshold);
await tx.exchanges.put(exchange);
});
return TaskRunResult.finished();

View File

@ -128,6 +128,8 @@ import {
} from "../util/coinSelection.js";
import {
ExchangeDetailsRecord,
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
PendingTaskType,
isWithdrawableDenom,
} from "../index.js";
@ -2341,10 +2343,6 @@ export async function internalPerformCreateWithdrawalGroup(
}>,
prep: PrepareCreateWithdrawalGroupResult,
): Promise<PerformCreateWithdrawalGroupResult> {
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId: prep.withdrawalGroup.withdrawalGroupId,
});
const { withdrawalGroup } = prep;
if (!prep.creationInfo) {
return { withdrawalGroup, transitionInfo: undefined };
@ -2361,6 +2359,7 @@ export async function internalPerformCreateWithdrawalGroup(
const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
if (exchange) {
exchange.lastWithdrawal = TalerPreciseTimestamp.now();
exchange.entryStatus = ExchangeEntryDbRecordStatus.Used;
await tx.exchanges.put(exchange);
}

View File

@ -189,6 +189,7 @@ import {
} from "./operations/deposits.js";
import {
acceptExchangeTermsOfService,
addPresetExchangeEntry,
downloadTosFromAcceptedFormat,
getExchangeDetails,
getExchangeRequestTimeout,
@ -533,6 +534,7 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
await tx.auditorTrust.put(c);
}
for (const baseUrl of ws.config.builtin.exchanges) {
await addPresetExchangeEntry(tx, baseUrl);
const now = AbsoluteTime.now();
provideExchangeRecordInTx(ws, tx, baseUrl, now);
}
@ -1688,8 +1690,7 @@ export class Wallet {
public static defaultConfig: Readonly<WalletConfig> = {
builtin: {
//exchanges: ["https://exchange.demo.taler.net/"],
exchanges: [],
exchanges: ["https://exchange.demo.taler.net/"],
auditors: [
{
currency: "KUDOS",