summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-12-11 20:01:28 +0100
committerFlorian Dold <florian@dold.me>2023-12-12 15:42:34 +0100
commite31f18b8f129adb9cbe33158297a9cff56a7143e (patch)
treefc960e069a08ca1924a79c154f5ced26db709348 /packages/taler-wallet-core/src/operations
parent055645e17aa9424f299aa04f686de7574ab437c7 (diff)
downloadwallet-core-e31f18b8f129adb9cbe33158297a9cff56a7143e.tar.gz
wallet-core-e31f18b8f129adb9cbe33158297a9cff56a7143e.tar.bz2
wallet-core-e31f18b8f129adb9cbe33158297a9cff56a7143e.zip
wallet-core: towards better DD48 support
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts200
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts723
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts29
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts12
-rw-r--r--packages/taler-wallet-core/src/operations/reward.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts10
9 files changed, 759 insertions, 231 deletions
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
index 6ab6a54d9..abba3f7a7 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -26,6 +26,7 @@ import {
CoinRefreshRequest,
CoinStatus,
Duration,
+ ExchangeEntryState,
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
@@ -75,7 +76,10 @@ import { PendingTaskType, TaskId } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
-import { constructTransactionIdentifier } from "./transactions.js";
+import {
+ constructTransactionIdentifier,
+ parseTransactionIdentifier,
+} from "./transactions.js";
const logger = new Logger("operations/common.ts");
@@ -320,11 +324,7 @@ function convertTaskToTransactionId(
}
}
-/**
- * For tasks that process a transaction,
- * generate a state transition notification.
- */
-async function taskToTransactionNotification(
+async function makeTransactionRetryNotification(
ws: InternalWalletState,
tx: GetReadOnlyAccess<typeof WalletStoresV1>,
pendingTaskId: string,
@@ -353,6 +353,75 @@ async function taskToTransactionNotification(
return notif;
}
+async function makeExchangeRetryNotification(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+ pendingTaskId: string,
+ e: TalerErrorDetail | undefined,
+): Promise<WalletNotification | undefined> {
+ logger.info("making exchange retry notification");
+ const parsedTaskId = parseTaskIdentifier(pendingTaskId);
+ if (parsedTaskId.tag !== PendingTaskType.ExchangeUpdate) {
+ throw Error("invalid task identifier");
+ }
+ const rec = await tx.exchanges.get(parsedTaskId.exchangeBaseUrl);
+
+ if (!rec) {
+ logger.info(`exchange ${parsedTaskId.exchangeBaseUrl} not found`);
+ return undefined;
+ }
+
+ const notif: WalletNotification = {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: parsedTaskId.exchangeBaseUrl,
+ oldExchangeState: getExchangeState(rec),
+ newExchangeState: getExchangeState(rec),
+ };
+ if (e) {
+ notif.errorInfo = {
+ code: e.code as number,
+ hint: e.hint,
+ };
+ }
+ return notif;
+}
+
+/**
+ * Generate an appropriate error transition notification
+ * for applicable tasks.
+ *
+ * Namely, transition notifications are generated for:
+ * - exchange update errors
+ * - transactions
+ */
+async function taskToRetryNotification(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+ pendingTaskId: string,
+ e: TalerErrorDetail | undefined,
+): Promise<WalletNotification | undefined> {
+ const parsedTaskId = parseTaskIdentifier(pendingTaskId);
+
+ switch (parsedTaskId.tag) {
+ case PendingTaskType.ExchangeUpdate:
+ return makeExchangeRetryNotification(ws, tx, pendingTaskId, e);
+ case PendingTaskType.PeerPullCredit:
+ case PendingTaskType.PeerPullDebit:
+ case PendingTaskType.Withdraw:
+ case PendingTaskType.PeerPushCredit:
+ case PendingTaskType.Deposit:
+ case PendingTaskType.Refresh:
+ case PendingTaskType.RewardPickup:
+ case PendingTaskType.PeerPushDebit:
+ case PendingTaskType.Purchase:
+ return makeTransactionRetryNotification(ws, tx, pendingTaskId, e);
+ case PendingTaskType.Backup:
+ case PendingTaskType.ExchangeCheckRefresh:
+ case PendingTaskType.Recoup:
+ return undefined;
+ }
+}
+
async function storePendingTaskError(
ws: InternalWalletState,
pendingTaskId: string,
@@ -372,7 +441,7 @@ async function storePendingTaskError(
retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
}
await tx.operationRetries.put(retryRecord);
- return taskToTransactionNotification(ws, tx, pendingTaskId, e);
+ return taskToRetryNotification(ws, tx, pendingTaskId, e);
});
if (maybeNotification) {
ws.notify(maybeNotification);
@@ -391,7 +460,7 @@ export async function resetPendingTaskTimeout(
retryRecord.retryInfo = DbRetryInfo.reset();
await tx.operationRetries.put(retryRecord);
}
- return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
+ return taskToRetryNotification(ws, tx, pendingTaskId, undefined);
});
if (maybeNotification) {
ws.notify(maybeNotification);
@@ -419,7 +488,7 @@ async function storePendingTaskPending(
}
await tx.operationRetries.put(retryRecord);
if (hadError) {
- return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
+ return taskToRetryNotification(ws, tx, pendingTaskId, undefined);
} else {
return undefined;
}
@@ -532,66 +601,72 @@ export enum TombstoneTag {
DeletePeerPushCredit = "delete-peer-push-credit",
}
-export function getExchangeTosStatus(
- exchangeDetails: ExchangeDetailsRecord,
+export function getExchangeTosStatusFromRecord(
+ exchange: ExchangeEntryRecord,
): ExchangeTosStatus {
- if (!exchangeDetails.tosAccepted) {
+ if (!exchange.tosAcceptedEtag) {
return ExchangeTosStatus.Proposed;
}
- if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {
+ if (exchange.tosAcceptedEtag == exchange.tosCurrentEtag) {
return ExchangeTosStatus.Accepted;
}
return ExchangeTosStatus.Proposed;
}
-export function makeExchangeListItem(
+export function getExchangeUpdateStatusFromRecord(
r: ExchangeEntryRecord,
- exchangeDetails: ExchangeDetailsRecord | undefined,
- lastError: TalerErrorDetail | undefined,
-): ExchangeListItem {
- const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
- ? {
- error: lastError,
- }
- : undefined;
-
- let exchangeUpdateStatus: ExchangeUpdateStatus;
+): ExchangeUpdateStatus {
switch (r.updateStatus) {
- case ExchangeEntryDbUpdateStatus.Failed:
- exchangeUpdateStatus = ExchangeUpdateStatus.Failed;
- break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ return ExchangeUpdateStatus.UnavailableUpdate;
case ExchangeEntryDbUpdateStatus.Initial:
- exchangeUpdateStatus = ExchangeUpdateStatus.Initial;
- break;
+ return ExchangeUpdateStatus.Initial;
case ExchangeEntryDbUpdateStatus.InitialUpdate:
- exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate;
- break;
- case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
- exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate;
- break;
+ return ExchangeUpdateStatus.InitialUpdate;
case ExchangeEntryDbUpdateStatus.Ready:
- exchangeUpdateStatus = ExchangeUpdateStatus.Ready;
- break;
+ return ExchangeUpdateStatus.Ready;
case ExchangeEntryDbUpdateStatus.ReadyUpdate:
- exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate;
- break;
+ return ExchangeUpdateStatus.ReadyUpdate;
case ExchangeEntryDbUpdateStatus.Suspended:
- exchangeUpdateStatus = ExchangeUpdateStatus.Suspended;
- break;
+ return ExchangeUpdateStatus.Suspended;
}
+}
- let exchangeEntryStatus: ExchangeEntryStatus;
+export function getExchangeEntryStatusFromRecord(
+ r: ExchangeEntryRecord,
+): ExchangeEntryStatus {
switch (r.entryStatus) {
case ExchangeEntryDbRecordStatus.Ephemeral:
- exchangeEntryStatus = ExchangeEntryStatus.Ephemeral;
- break;
+ return ExchangeEntryStatus.Ephemeral;
case ExchangeEntryDbRecordStatus.Preset:
- exchangeEntryStatus = ExchangeEntryStatus.Preset;
- break;
+ return ExchangeEntryStatus.Preset;
case ExchangeEntryDbRecordStatus.Used:
- exchangeEntryStatus = ExchangeEntryStatus.Used;
- break;
+ return ExchangeEntryStatus.Used;
}
+}
+
+/**
+ * Compute the state of an exchange entry from the DB
+ * record.
+ */
+export function getExchangeState(r: ExchangeEntryRecord): ExchangeEntryState {
+ return {
+ exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
+ exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
+ tosStatus: getExchangeTosStatusFromRecord(r),
+ };
+}
+
+export function makeExchangeListItem(
+ r: ExchangeEntryRecord,
+ exchangeDetails: ExchangeDetailsRecord | undefined,
+ lastError: TalerErrorDetail | undefined,
+): ExchangeListItem {
+ const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
+ ? {
+ error: lastError,
+ }
+ : undefined;
let scopeInfo: ScopeInfo | undefined = undefined;
if (exchangeDetails) {
@@ -606,11 +681,9 @@ export function makeExchangeListItem(
return {
exchangeBaseUrl: r.baseUrl,
currency: exchangeDetails?.currency ?? r.presetCurrencyHint,
- exchangeUpdateStatus,
- exchangeEntryStatus,
- tosStatus: exchangeDetails
- ? getExchangeTosStatus(exchangeDetails)
- : ExchangeTosStatus.Pending,
+ exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
+ exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
+ tosStatus: getExchangeTosStatusFromRecord(r),
ageRestrictionOptions: exchangeDetails?.ageMask
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
: [],
@@ -852,7 +925,6 @@ export type ParsedTaskIdentifier =
| { tag: PendingTaskType.Backup; backupProviderBaseUrl: string }
| { tag: PendingTaskType.Deposit; depositGroupId: string }
| { tag: PendingTaskType.ExchangeCheckRefresh; exchangeBaseUrl: string }
- | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
| { tag: PendingTaskType.PeerPullDebit; peerPullDebitId: string }
| { tag: PendingTaskType.PeerPullCredit; pursePub: string }
| { tag: PendingTaskType.PeerPushCredit; peerPushCreditId: string }
@@ -872,13 +944,13 @@ export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
const [type, ...rest] = task;
switch (type) {
case PendingTaskType.Backup:
- return { tag: type, backupProviderBaseUrl: rest[0] };
+ return { tag: type, backupProviderBaseUrl: decodeURIComponent(rest[0]) };
case PendingTaskType.Deposit:
return { tag: type, depositGroupId: rest[0] };
case PendingTaskType.ExchangeCheckRefresh:
- return { tag: type, exchangeBaseUrl: rest[0] };
+ return { tag: type, exchangeBaseUrl: decodeURIComponent(rest[0]) };
case PendingTaskType.ExchangeUpdate:
- return { tag: type, exchangeBaseUrl: rest[0] };
+ return { tag: type, exchangeBaseUrl: decodeURIComponent(rest[0]) };
case PendingTaskType.PeerPullCredit:
return { tag: type, pursePub: rest[0] };
case PendingTaskType.PeerPullDebit:
@@ -940,13 +1012,19 @@ export namespace TaskIdentifiers {
return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
}
export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskId {
- return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
+ return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent(
+ exch.baseUrl,
+ )}` as TaskId;
}
export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
- return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
+ return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent(
+ exchBaseUrl,
+ )}` as TaskId;
}
export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId {
- return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
+ return `${PendingTaskType.ExchangeCheckRefresh}:${encodeURIComponent(
+ exch.baseUrl,
+ )}` as TaskId;
}
export function forTipPickup(tipRecord: RewardRecord): TaskId {
return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` as TaskId;
@@ -964,7 +1042,9 @@ export namespace TaskIdentifiers {
return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}` as TaskId;
}
export function forBackup(backupRecord: BackupProviderRecord): TaskId {
- return `${PendingTaskType.Backup}:${backupRecord.baseUrl}` as TaskId;
+ return `${PendingTaskType.Backup}:${encodeURIComponent(
+ backupRecord.baseUrl,
+ )}` as TaskId;
}
export function forPeerPushPaymentInitiation(
ppi: PeerPushDebitRecord,
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 622f04bd3..253801e93 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -15,6 +15,12 @@
*/
/**
+ * @fileoverview
+ * Implementation of exchange entry management in wallet-core.
+ * The details of exchange entry management are specified in DD48.
+ */
+
+/**
* Imports.
*/
import {
@@ -31,7 +37,9 @@ import {
ExchangeAuditor,
ExchangeGlobalFees,
ExchangeSignKeyJson,
+ ExchangeEntryState,
ExchangeWireAccount,
+ GetExchangeTosResult,
GlobalFees,
hashDenomPub,
j2s,
@@ -48,10 +56,17 @@ import {
TalerProtocolDuration,
TalerProtocolTimestamp,
URL,
+ WalletNotification,
WireFee,
WireFeeMap,
WireFeesJson,
WireInfo,
+ FeeDescription,
+ DenomOperationMap,
+ DenominationInfo,
+ ExchangeDetailedResponse,
+ ExchangeListItem,
+ ExchangesListResponse,
} from "@gnu-taler/taler-util";
import {
getExpiry,
@@ -67,24 +82,29 @@ import {
WalletStoresV1,
} from "../db.js";
import {
+ createTimeline,
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
isWithdrawableDenom,
+ OpenedPromise,
+ openPromise,
+ selectBestForOverlappingDenominations,
+ selectMinimumFee,
timestampPreciseFromDb,
timestampPreciseToDb,
timestampProtocolToDb,
WalletDbReadWriteTransaction,
} from "../index.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
+import { CancelFn, InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
-import {
- DbAccess,
- GetReadOnlyAccess,
- GetReadWriteAccess,
-} from "../util/query.js";
+import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
import {
- runTaskWithErrorReporting,
+ getExchangeEntryStatusFromRecord,
+ getExchangeState,
+ getExchangeTosStatusFromRecord,
+ getExchangeUpdateStatusFromRecord,
+ makeExchangeListItem,
TaskIdentifiers,
TaskRunResult,
TaskRunResultType,
@@ -92,19 +112,19 @@ import {
const logger = new Logger("exchanges.ts");
-export function getExchangeRequestTimeout(): Duration {
+function getExchangeRequestTimeout(): Duration {
return Duration.fromSpec({
seconds: 5,
});
}
-export interface ExchangeTosDownloadResult {
+interface ExchangeTosDownloadResult {
tosText: string;
tosEtag: string;
tosContentType: string;
}
-export async function downloadExchangeWithTermsOfService(
+async function downloadExchangeWithTermsOfService(
exchangeBaseUrl: string,
http: HttpRequestLibrary,
timeout: Duration,
@@ -129,6 +149,8 @@ export async function downloadExchangeWithTermsOfService(
/**
* Get exchange details from the database.
+ *
+ * FIXME: Should we encapsulate the result better, instead of returning the raw DB records here?
*/
export async function getExchangeDetails(
tx: GetReadOnlyAccess<{
@@ -153,9 +175,6 @@ export async function getExchangeDetails(
]);
}
-getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
- db.mktx((x) => [x.exchanges, x.exchangeDetails]);
-
/**
* Mark a ToS version as accepted by the user.
*
@@ -169,13 +188,13 @@ export async function acceptExchangeTermsOfService(
await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails])
.runReadWrite(async (tx) => {
- const d = await getExchangeDetails(tx, exchangeBaseUrl);
- if (d) {
- d.tosAccepted = {
- etag: etag || d.tosCurrentEtag,
- timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
- };
- await tx.exchangeDetails.put(d);
+ const exch = await tx.exchanges.get(exchangeBaseUrl);
+ if (exch && exch.tosCurrentEtag) {
+ exch.tosAcceptedEtag = exch.tosCurrentEtag;
+ exch.tosAcceptedTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ await tx.exchanges.put(exch);
}
});
}
@@ -284,29 +303,18 @@ async function validateGlobalFees(
return egf;
}
-export interface ExchangeInfo {
- keys: ExchangeKeysDownloadResult;
-}
-
-export async function downloadExchangeInfo(
- exchangeBaseUrl: string,
- http: HttpRequestLibrary,
-): Promise<ExchangeInfo> {
- const keysInfo = await downloadExchangeKeysInfo(
- exchangeBaseUrl,
- http,
- Duration.getForever(),
- );
- return {
- keys: keysInfo,
- };
-}
-
+/**
+ * Add an exchange entry to the wallet database in the
+ * entry state "preset".
+ *
+ * Returns the notification to the caller that should be emitted
+ * if the DB transaction succeeds.
+ */
export async function addPresetExchangeEntry(
tx: WalletDbReadWriteTransaction<"exchanges">,
exchangeBaseUrl: string,
currencyHint?: string,
-): Promise<void> {
+): Promise<{ notification?: WalletNotification }> {
let exchange = await tx.exchanges.get(exchangeBaseUrl);
if (!exchange) {
const r: ExchangeEntryRecord = {
@@ -323,9 +331,22 @@ export async function addPresetExchangeEntry(
nextUpdateStamp: timestampPreciseToDb(
AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
),
+ tosAcceptedEtag: undefined,
+ tosAcceptedTimestamp: undefined,
+ tosCurrentEtag: undefined,
};
await tx.exchanges.put(r);
+ return {
+ notification: {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: exchangeBaseUrl,
+ // Exchange did not exist yet
+ oldExchangeState: undefined,
+ newExchangeState: getExchangeState(r),
+ },
+ };
}
+ return {};
}
async function provideExchangeRecordInTx(
@@ -339,7 +360,9 @@ async function provideExchangeRecordInTx(
): Promise<{
exchange: ExchangeEntryRecord;
exchangeDetails: ExchangeDetailsRecord | undefined;
+ notification?: WalletNotification;
}> {
+ let notification: WalletNotification | undefined = undefined;
let exchange = await tx.exchanges.get(baseUrl);
if (!exchange) {
const r: ExchangeEntryRecord = {
@@ -355,15 +378,24 @@ async function provideExchangeRecordInTx(
AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
),
lastKeysEtag: undefined,
+ tosAcceptedEtag: undefined,
+ tosAcceptedTimestamp: undefined,
+ tosCurrentEtag: undefined,
};
await tx.exchanges.put(r);
exchange = r;
+ notification = {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: r.baseUrl,
+ oldExchangeState: undefined,
+ newExchangeState: getExchangeState(r),
+ };
}
const exchangeDetails = await getExchangeDetails(tx, baseUrl);
- return { exchange, exchangeDetails };
+ return { exchange, exchangeDetails, notification };
}
-interface ExchangeKeysDownloadResult {
+export interface ExchangeKeysDownloadResult {
baseUrl: string;
masterPublicKey: string;
currency: string;
@@ -393,28 +425,36 @@ async function downloadExchangeKeysInfo(
const resp = await http.fetch(keysUrl.href, {
timeout,
});
- const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeKeysJson(),
- );
- if (exchangeKeysJsonUnchecked.denominations.length === 0) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
- {
- exchangeBaseUrl: baseUrl,
- },
- "exchange doesn't offer any denominations",
- );
- }
+ // We must make sure to parse out the protocol version
+ // before we validate the body.
+ // Otherwise the parser might complain with a hard to understand
+ // message about some other field, when it is just a version
+ // incompatibility.
- const protocolVersion = exchangeKeysJsonUnchecked.version;
+ const keysJson = await resp.json();
+
+ const protocolVersion = keysJson.version;
+ if (typeof protocolVersion !== "string") {
+ throw Error("bad exchange, does not even specify protocol version");
+ }
const versionRes = LibtoolVersion.compare(
WALLET_EXCHANGE_PROTOCOL_VERSION,
protocolVersion,
);
- if (versionRes?.compatible != true) {
+ if (!versionRes) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: resp.requestUrl,
+ httpStatusCode: resp.status,
+ requestMethod: resp.requestMethod,
+ },
+ "exchange protocol version malformed",
+ );
+ }
+ if (!versionRes.compatible) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
{
@@ -425,6 +465,21 @@ async function downloadExchangeKeysInfo(
);
}
+ const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeKeysJson(),
+ );
+
+ if (exchangeKeysJsonUnchecked.denominations.length === 0) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
+ {
+ exchangeBaseUrl: baseUrl,
+ },
+ "exchange doesn't offer any denominations",
+ );
+ }
+
const currency = exchangeKeysJsonUnchecked.currency;
const currentDenominations: DenominationRecord[] = [];
@@ -512,7 +567,7 @@ async function downloadExchangeKeysInfo(
};
}
-export async function downloadTosFromAcceptedFormat(
+async function downloadTosFromAcceptedFormat(
ws: InternalWalletState,
baseUrl: string,
timeout: Duration,
@@ -546,54 +601,225 @@ 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.
+ * Transition an exchange into an updating state.
+ *
+ * If the update is forced, the exchange is put into an updating state
+ * even if the old information should still be up to date.
+ *
+ * For backwards compatibility, if the exchange entry doesn't exist,
+ * a new ephemeral entry is created.
*/
-export async function updateExchangeFromUrl(
+export async function startUpdateExchangeEntry(
ws: InternalWalletState,
- baseUrl: string,
- options: {
- checkMasterPub?: string;
- forceNow?: boolean;
- cancellationToken?: CancellationToken;
- } = {},
+ exchangeBaseUrl: string,
+ options: { forceUpdate?: boolean } = {},
+): Promise<void> {
+ const canonBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+
+ const now = AbsoluteTime.now();
+
+ const { notification } = await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails])
+ .runReadWrite(async (tx) => {
+ return provideExchangeRecordInTx(ws, tx, exchangeBaseUrl, now);
+ });
+
+ if (notification) {
+ ws.notify(notification);
+ }
+
+ const { oldExchangeState, newExchangeState } = await ws.db
+ .mktx((x) => [x.exchanges, x.operationRetries])
+ .runReadWrite(async (tx) => {
+ const r = await tx.exchanges.get(canonBaseUrl);
+ if (!r) {
+ throw Error("exchange not found");
+ }
+ const oldExchangeState = getExchangeState(r);
+ switch (r.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ break;
+ case ExchangeEntryDbUpdateStatus.Suspended:
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ break;
+ case ExchangeEntryDbUpdateStatus.Ready: {
+ const nextUpdateTimestamp = AbsoluteTime.fromPreciseTimestamp(
+ timestampPreciseFromDb(r.nextUpdateStamp),
+ );
+ // Only update if entry is outdated or update is forced.
+ if (
+ options.forceUpdate ||
+ AbsoluteTime.isExpired(nextUpdateTimestamp)
+ ) {
+ r.updateStatus = ExchangeEntryDbUpdateStatus.ReadyUpdate;
+ }
+ break;
+ }
+ case ExchangeEntryDbUpdateStatus.Initial:
+ r.updateStatus = ExchangeEntryDbUpdateStatus.InitialUpdate;
+ break;
+ }
+ await tx.exchanges.put(r);
+ const newExchangeState = getExchangeState(r);
+ // Reset retries for updating the exchange entry.
+ const taskId = TaskIdentifiers.forExchangeUpdate(r);
+ await tx.operationRetries.delete(taskId);
+ return { oldExchangeState, newExchangeState };
+ });
+ ws.notify({
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: canonBaseUrl,
+ newExchangeState: newExchangeState,
+ oldExchangeState: oldExchangeState,
+ });
+ ws.workAvailable.trigger();
+}
+
+export interface NotificationWaiter {
+ waitNext(): Promise<void>;
+ cancel(): void;
+}
+
+export function createNotificationWaiter(
+ ws: InternalWalletState,
+ pred: (x: WalletNotification) => boolean,
+): NotificationWaiter {
+ ws.ensureTaskLoopRunning();
+ let cancelFn: CancelFn | undefined = undefined;
+ let p: OpenedPromise<void> | undefined = undefined;
+
+ return {
+ cancel() {
+ cancelFn?.();
+ },
+ waitNext(): Promise<void> {
+ if (!p) {
+ p = openPromise();
+ cancelFn = ws.addNotificationListener((notif) => {
+ if (pred(notif)) {
+ // We got a notification that matches our predicate.
+ // Resolve promise for existing waiters,
+ // and create a new promise to wait for the next
+ // notification occurrence.
+ const myResolve = p?.resolve;
+ const myCancel = cancelFn;
+ p = undefined;
+ cancelFn = undefined;
+ myResolve?.();
+ myCancel?.();
+ }
+ });
+ }
+ return p.promise;
+ },
+ };
+}
+
+/**
+ * Wait until an exchange entry got successfully updated.
+ *
+ * Reject with an exception if the update encountered an error.
+ */
+export async function waitExchangeEntryUpdated(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ cancellationToken?: CancellationToken,
): Promise<{
exchange: ExchangeEntryRecord;
exchangeDetails: ExchangeDetailsRecord;
}> {
- const canonUrl = canonicalizeBaseUrl(baseUrl);
- const res = await runTaskWithErrorReporting(
+ exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+
+ const waiter = createNotificationWaiter(
ws,
- TaskIdentifiers.forExchangeUpdateFromUrl(canonUrl),
- () => updateExchangeFromUrlHandler(ws, canonUrl, options),
+ (notif) =>
+ notif.type == NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl,
);
- switch (res.type) {
- case TaskRunResultType.Finished: {
- const now = AbsoluteTime.now();
- const { exchange, exchangeDetails } = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadWrite(async (tx) => {
- let exchange = await tx.exchanges.get(canonUrl);
- const exchangeDetails = await getExchangeDetails(tx, canonUrl);
- return { exchange, exchangeDetails };
- });
- if (!exchange) {
- throw Error("exchange not found");
- }
- if (!exchangeDetails) {
- throw Error("exchange details not found");
+
+ const taskId = TaskIdentifiers.forExchangeUpdateFromUrl(exchangeBaseUrl);
+
+ while (1) {
+ const { exchange, retryRecord } = await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails, x.operationRetries])
+ .runReadOnly(async (tx) => {
+ const exchange = await tx.exchanges.get(exchangeBaseUrl);
+ const retryRecord = await tx.operationRetries.get(taskId);
+ return { exchange, retryRecord };
+ });
+
+ if (!exchange) {
+ throw Error("exchange does not exist anymore");
+ }
+
+ switch (exchange.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ const details = await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails])
+ .runReadOnly(async (tx) => {
+ return getExchangeDetails(tx, exchangeBaseUrl);
+ });
+ if (!details) {
+ throw Error("exchange entry inconsistent");
+ }
+ waiter.cancel();
+ return { exchange, exchangeDetails: details };
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ case ExchangeEntryDbUpdateStatus.InitialUpdate: {
+ waiter.cancel();
+ if (retryRecord?.lastError) {
+ throw TalerError.fromUncheckedDetail(retryRecord.lastError);
+ }
+ break;
}
- return { exchange, exchangeDetails };
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ waiter.cancel();
+ if (retryRecord?.lastError) {
+ throw TalerError.fromUncheckedDetail(retryRecord.lastError);
+ } else {
+ throw Error(
+ "updating exchange failed, error info unavailable (bug!)",
+ );
+ }
}
- case TaskRunResultType.Error:
- throw TalerError.fromUncheckedDetail(res.errorDetail);
- default:
- throw Error(`unexpected operation result (${res.type})`);
+
+ await waiter.waitNext();
}
+ throw Error("not reached");
}
/**
- * Update or add exchange DB entry by fetching the /keys and /wire information.
+ * Ensure that a fresh exchange entry exists for the given
+ * exchange base URL.
+ *
+ * The cancellation token can be used to abort waiting for the
+ * updated exchange entry.
+ *
+ * If an exchange entry for the database doesn't exist in the
+ * DB, it will be added ephemerally.
+ */
+export async function fetchFreshExchange(
+ ws: InternalWalletState,
+ baseUrl: string,
+ options: {
+ cancellationToken?: CancellationToken;
+ forceUpdate?: boolean;
+ } = {},
+): Promise<{
+ exchange: ExchangeEntryRecord;
+ exchangeDetails: ExchangeDetailsRecord;
+}> {
+ const canonUrl = canonicalizeBaseUrl(baseUrl);
+ await startUpdateExchangeEntry(ws, canonUrl, {
+ forceUpdate: options.forceUpdate,
+ });
+ return waitExchangeEntryUpdated(ws, canonUrl, options.cancellationToken);
+}
+
+/**
+ * Update an exchange entry in the wallet's database
+ * by fetching the /keys and /wire information.
* Optionally link the reserve entry to the new or existing
* exchange entry in then DB.
*/
@@ -601,48 +827,11 @@ export async function updateExchangeFromUrlHandler(
ws: InternalWalletState,
exchangeBaseUrl: string,
options: {
- checkMasterPub?: string;
- forceNow?: boolean;
cancellationToken?: CancellationToken;
} = {},
): Promise<TaskRunResult> {
- const forceNow = options.forceNow ?? false;
- logger.trace(
- `updating exchange info for ${exchangeBaseUrl}, forced: ${forceNow}`,
- );
-
- const now = AbsoluteTime.now();
+ logger.trace(`updating exchange info for ${exchangeBaseUrl}`);
exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
- let isNewExchange = true;
- const { exchange, exchangeDetails } = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadWrite(async (tx) => {
- let oldExch = await tx.exchanges.get(exchangeBaseUrl);
- if (oldExch) {
- isNewExchange = false;
- }
- return provideExchangeRecordInTx(ws, tx, exchangeBaseUrl, now);
- });
-
- if (
- !forceNow &&
- exchangeDetails !== undefined &&
- !AbsoluteTime.isExpired(
- AbsoluteTime.fromPreciseTimestamp(
- timestampPreciseFromDb(exchange.nextUpdateStamp),
- ),
- )
- ) {
- logger.trace("using existing exchange info");
-
- if (options.checkMasterPub) {
- if (exchangeDetails.masterPublicKey !== options.checkMasterPub) {
- throw Error(`master public key mismatch`);
- }
- }
-
- return TaskRunResult.finished();
- }
logger.trace("updating exchange /keys info");
@@ -654,12 +843,6 @@ export async function updateExchangeFromUrlHandler(
timeout,
);
- if (options.checkMasterPub) {
- if (keysInfo.masterPublicKey !== options.checkMasterPub) {
- throw Error(`master public key mismatch`);
- }
- }
-
logger.trace("validating exchange wire info");
const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion);
@@ -740,6 +923,7 @@ export async function updateExchangeFromUrlHandler(
logger.warn(`exchange ${exchangeBaseUrl} no longer present`);
return;
}
+ const oldExchangeState = getExchangeState(r);
const existingDetails = await getExchangeDetails(tx, r.baseUrl);
if (!existingDetails) {
detailsPointerChanged = true;
@@ -753,7 +937,6 @@ export async function updateExchangeFromUrlHandler(
}
// FIXME: We need to do some consistency checks!
}
- const existingTosAccepted = existingDetails?.tosAccepted;
const newDetails: ExchangeDetailsRecord = {
auditors: keysInfo.auditors,
currency: keysInfo.currency,
@@ -763,10 +946,9 @@ export async function updateExchangeFromUrlHandler(
globalFees,
exchangeBaseUrl: r.baseUrl,
wireInfo,
- tosCurrentEtag: tosDownload.tosEtag,
- tosAccepted: existingTosAccepted,
ageMask,
};
+ r.tosCurrentEtag = tosDownload.tosEtag;
if (existingDetails?.rowId) {
newDetails.rowId = existingDetails.rowId;
}
@@ -787,6 +969,7 @@ export async function updateExchangeFromUrlHandler(
updateClock: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
}
+ r.updateStatus = ExchangeEntryDbUpdateStatus.Ready;
await tx.exchanges.put(r);
const drRowId = await tx.exchangeDetails.put(newDetails);
checkDbInvariant(typeof drRowId.key === "number");
@@ -881,14 +1064,18 @@ export async function updateExchangeFromUrlHandler(
recoupGroupId = await ws.recoupOps.createRecoupGroup(
ws,
tx,
- exchange.baseUrl,
+ exchangeBaseUrl,
newlyRevokedCoinPubs,
);
}
+ const newExchangeState = getExchangeState(r);
+
return {
exchange: r,
exchangeDetails: newDetails,
+ oldExchangeState,
+ newExchangeState,
};
});
@@ -904,11 +1091,12 @@ export async function updateExchangeFromUrlHandler(
logger.trace("done updating exchange info in database");
- if (isNewExchange) {
- ws.notify({
- type: NotificationType.ExchangeAdded,
- });
- }
+ ws.notify({
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl,
+ newExchangeState: updated.newExchangeState,
+ oldExchangeState: updated.oldExchangeState,
+ });
return TaskRunResult.finished();
}
@@ -926,8 +1114,8 @@ export async function getExchangePaytoUri(
): Promise<string> {
// We do the update here, since the exchange might not even exist
// yet in our database.
- const details = await getExchangeDetails
- .makeContext(ws.db)
+ const details = await ws.db
+ .mktx((x) => [x.exchangeDetails, x.exchanges])
.runReadOnly(async (tx) => {
return getExchangeDetails(tx, exchangeBaseUrl);
});
@@ -947,3 +1135,246 @@ export async function getExchangePaytoUri(
)}`,
);
}
+
+/**
+ * Get the exchange ToS in the requested format.
+ * Try to download in the accepted format not cached.
+ */
+export async function getExchangeTos(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ acceptedFormat?: string[],
+): Promise<GetExchangeTosResult> {
+ // FIXME: download ToS in acceptable format if passed!
+ const { exchange, exchangeDetails } = await fetchFreshExchange(
+ ws,
+ exchangeBaseUrl,
+ );
+
+ const tosDownload = await downloadTosFromAcceptedFormat(
+ ws,
+ exchangeBaseUrl,
+ getExchangeRequestTimeout(),
+ acceptedFormat,
+ );
+
+ await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails])
+ .runReadWrite(async (tx) => {
+ const updateExchangeEntry = await tx.exchanges.get(exchangeBaseUrl);
+ if (updateExchangeEntry) {
+ updateExchangeEntry.tosCurrentEtag = tosDownload.tosEtag;
+ await tx.exchanges.put(updateExchangeEntry);
+ }
+ });
+
+ return {
+ acceptedEtag: exchange.tosAcceptedEtag,
+ currentEtag: tosDownload.tosEtag,
+ content: tosDownload.tosText,
+ contentType: tosDownload.tosContentType,
+ tosStatus: getExchangeTosStatusFromRecord(exchange),
+ };
+}
+
+export interface ExchangeInfo {
+ keys: ExchangeKeysDownloadResult;
+}
+
+/**
+ * Helper function to download the exchange /keys info.
+ *
+ * Only used for testing / dbless wallet.
+ */
+export async function downloadExchangeInfo(
+ exchangeBaseUrl: string,
+ http: HttpRequestLibrary,
+): Promise<ExchangeInfo> {
+ const keysInfo = await downloadExchangeKeysInfo(
+ exchangeBaseUrl,
+ http,
+ Duration.getForever(),
+ );
+ return {
+ keys: keysInfo,
+ };
+}
+
+export async function getExchanges(
+ ws: InternalWalletState,
+): Promise<ExchangesListResponse> {
+ const exchanges: ExchangeListItem[] = [];
+ await ws.db
+ .mktx((x) => [
+ x.exchanges,
+ x.exchangeDetails,
+ x.denominations,
+ x.operationRetries,
+ ])
+ .runReadOnly(async (tx) => {
+ const exchangeRecords = await tx.exchanges.iter().toArray();
+ for (const r of exchangeRecords) {
+ const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
+ const opRetryRecord = await tx.operationRetries.get(
+ TaskIdentifiers.forExchangeUpdate(r),
+ );
+ exchanges.push(
+ makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError),
+ );
+ }
+ });
+ return { exchanges };
+}
+
+export async function getExchangeDetailedInfo(
+ ws: InternalWalletState,
+ exchangeBaseurl: string,
+): Promise<ExchangeDetailedResponse> {
+ //TODO: should we use the forceUpdate parameter?
+ const exchange = await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
+ .runReadOnly(async (tx) => {
+ const ex = await tx.exchanges.get(exchangeBaseurl);
+ const dp = ex?.detailsPointer;
+ if (!dp) {
+ return;
+ }
+ const { currency } = dp;
+ const exchangeDetails = await getExchangeDetails(tx, ex.baseUrl);
+ if (!exchangeDetails) {
+ return;
+ }
+
+ const denominationRecords =
+ await tx.denominations.indexes.byExchangeBaseUrl
+ .iter(ex.baseUrl)
+ .toArray();
+
+ if (!denominationRecords) {
+ return;
+ }
+
+ const denominations: DenominationInfo[] = denominationRecords.map((x) =>
+ DenominationRecord.toDenomInfo(x),
+ );
+
+ return {
+ info: {
+ exchangeBaseUrl: ex.baseUrl,
+ currency,
+ paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
+ auditors: exchangeDetails.auditors,
+ wireInfo: exchangeDetails.wireInfo,
+ globalFees: exchangeDetails.globalFees,
+ },
+ denominations,
+ };
+ });
+
+ if (!exchange) {
+ throw Error(`exchange with base url "${exchangeBaseurl}" not found`);
+ }
+
+ const denoms = exchange.denominations.map((d) => ({
+ ...d,
+ group: Amounts.stringifyValue(d.value),
+ }));
+ const denomFees: DenomOperationMap<FeeDescription[]> = {
+ deposit: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireDeposit",
+ "feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ refresh: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireWithdraw",
+ "feeRefresh",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ refund: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireWithdraw",
+ "feeRefund",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ withdraw: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireWithdraw",
+ "feeWithdraw",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ };
+
+ const transferFees = Object.entries(
+ exchange.info.wireInfo.feesForType,
+ ).reduce((prev, [wireType, infoForType]) => {
+ const feesByGroup = [
+ ...infoForType.map((w) => ({
+ ...w,
+ fee: Amounts.stringify(w.closingFee),
+ group: "closing",
+ })),
+ ...infoForType.map((w) => ({ ...w, fee: w.wireFee, group: "wire" })),
+ ];
+ prev[wireType] = createTimeline(
+ feesByGroup,
+ "sig",
+ "startStamp",
+ "endStamp",
+ "fee",
+ "group",
+ selectMinimumFee,
+ );
+ return prev;
+ }, {} as Record<string, FeeDescription[]>);
+
+ const globalFeesByGroup = [
+ ...exchange.info.globalFees.map((w) => ({
+ ...w,
+ fee: w.accountFee,
+ group: "account",
+ })),
+ ...exchange.info.globalFees.map((w) => ({
+ ...w,
+ fee: w.historyFee,
+ group: "history",
+ })),
+ ...exchange.info.globalFees.map((w) => ({
+ ...w,
+ fee: w.purseFee,
+ group: "purse",
+ })),
+ ];
+
+ const globalFees = createTimeline(
+ globalFeesByGroup,
+ "signature",
+ "startDate",
+ "endDate",
+ "fee",
+ "group",
+ selectMinimumFee,
+ );
+
+ return {
+ exchange: {
+ ...exchange.info,
+ denomFees,
+ transferFees,
+ globalFees,
+ },
+ };
+}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
index 44c9436b1..f8ab07b10 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
@@ -63,7 +63,7 @@ import {
timestampOptionalPreciseFromDb,
timestampPreciseFromDb,
timestampPreciseToDb,
- updateExchangeFromUrl,
+ fetchFreshExchange,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
@@ -764,7 +764,7 @@ export async function initiatePeerPullPayment(
const exchangeBaseUrl = maybeExchangeBaseUrl;
- await updateExchangeFromUrl(ws, exchangeBaseUrl);
+ await fetchFreshExchange(ws, exchangeBaseUrl);
const mergeReserveInfo = await getMergeReserveInfo(ws, {
exchangeBaseUrl: exchangeBaseUrl,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
index 690edf2e7..575780ba4 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -69,7 +69,7 @@ import {
constructTaskIdentifier,
runLongpollAsync,
} from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
+import { fetchFreshExchange } from "./exchanges.js";
import {
codecForExchangePurseStatus,
getMergeReserveInfo,
@@ -141,7 +141,7 @@ export async function preparePeerPushCredit(
const exchangeBaseUrl = uri.exchangeBaseUrl;
- await updateExchangeFromUrl(ws, exchangeBaseUrl);
+ await fetchFreshExchange(ws, exchangeBaseUrl);
const contractPriv = uri.contractPriv;
const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index 282f84ad7..a9d6c5595 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -22,12 +22,17 @@
* Imports.
*/
import { GlobalIDB } from "@gnu-taler/idb-bridge";
-import { AbsoluteTime, TransactionRecordFilter } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ TransactionRecordFilter,
+} from "@gnu-taler/taler-util";
import {
BackupProviderStateTag,
+ DbPreciseTimestamp,
DepositElementStatus,
DepositGroupRecord,
- DepositOperationStatus,
ExchangeEntryDbUpdateStatus,
PeerPullCreditRecord,
PeerPullDebitRecordStatus,
@@ -48,7 +53,6 @@ import {
RewardRecordStatus,
WalletStoresV1,
WithdrawalGroupRecord,
- WithdrawalGroupStatus,
depositOperationNonfinalStatusRange,
timestampAbsoluteFromDb,
timestampOptionalAbsoluteFromDb,
@@ -94,18 +98,29 @@ async function gatherExchangePending(
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
- // FIXME: We should do a range query here based on the update time
- // and/or the entry state.
+ let timestampDue: DbPreciseTimestamp | undefined = undefined;
await tx.exchanges.iter().forEachAsync(async (exch) => {
switch (exch.updateStatus) {
case ExchangeEntryDbUpdateStatus.Initial:
case ExchangeEntryDbUpdateStatus.Suspended:
- case ExchangeEntryDbUpdateStatus.Failed:
return;
}
const opUpdateExchangeTag = TaskIdentifiers.forExchangeUpdate(exch);
let opr = await tx.operationRetries.get(opUpdateExchangeTag);
- const timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp;
+
+ switch (exch.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp;
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ case ExchangeEntryDbUpdateStatus.InitialUpdate:
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ timestampDue =
+ opr?.retryInfo.nextRetry ??
+ timestampPreciseToDb(TalerPreciseTimestamp.now());
+ break;
+ }
+
resp.pendingOperations.push({
type: PendingTaskType.ExchangeUpdate,
...getPendingCommon(
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 3afdd2d71..51dd9adac 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -98,7 +98,11 @@ import {
TaskRunResult,
TaskRunResultType,
} from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
+import {
+ fetchFreshExchange,
+ startUpdateExchangeEntry,
+ waitExchangeEntryUpdated,
+} from "./exchanges.js";
import {
constructTransactionIdentifier,
notifyTransition,
@@ -221,7 +225,7 @@ async function provideRefreshSession(
const { refreshGroup, coin } = d;
- const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
+ const { exchange } = await fetchFreshExchange(ws, coin.exchangeBaseUrl);
if (!exchange) {
throw Error("db inconsistent: exchange of coin not found");
}
@@ -1157,9 +1161,7 @@ export async function autoRefresh(
// We must make sure that the exchange is up-to-date so that
// can refresh into new denominations.
- await updateExchangeFromUrl(ws, exchangeBaseUrl, {
- forceNow: true,
- });
+ await fetchFreshExchange(ws, exchangeBaseUrl);
let minCheckThreshold = AbsoluteTime.addDuration(
AbsoluteTime.now(),
diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts
index 5d609f41d..90320d7cb 100644
--- a/packages/taler-wallet-core/src/operations/reward.ts
+++ b/packages/taler-wallet-core/src/operations/reward.ts
@@ -69,7 +69,7 @@ import {
TaskRunResult,
TaskRunResultType,
} from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
+import { fetchFreshExchange } from "./exchanges.js";
import {
getCandidateWithdrawalDenoms,
getExchangeWithdrawalInfo,
@@ -175,7 +175,7 @@ export async function prepareTip(
const amount = Amounts.parseOrThrow(tipPickupStatus.reward_amount);
logger.trace("new tip, creating tip record");
- await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
+ await fetchFreshExchange(ws, tipPickupStatus.exchange_url);
//FIXME: is this needed? withdrawDetails is not used
// * if the intention is to update the exchange information in the database
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index b30c5f80b..a03d54d3a 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -58,7 +58,7 @@ import { OpenedPromise, openPromise } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { checkLogicInvariant } from "../util/invariants.js";
import { getBalances } from "./balance.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
+import { fetchFreshExchange } from "./exchanges.js";
import {
confirmPay,
preparePayForUri,
@@ -579,7 +579,7 @@ export async function runIntegrationTest2(
// waiting for notifications.
logger.info("running test with arguments", args);
- const exchangeInfo = await updateExchangeFromUrl(ws, args.exchangeBaseUrl);
+ const exchangeInfo = await fetchFreshExchange(ws, args.exchangeBaseUrl);
const currency = exchangeInfo.exchangeDetails.currency;
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index b9ba3058f..e7ba6d820 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -135,7 +135,7 @@ import {
import {
getExchangeDetails,
getExchangePaytoUri,
- updateExchangeFromUrl,
+ fetchFreshExchange,
} from "./exchanges.js";
import {
TransitionInfo,
@@ -1862,8 +1862,8 @@ export async function getExchangeWithdrawalInfo(
}
let tosAccepted = false;
- if (exchangeDetails.tosAccepted?.timestamp) {
- if (exchangeDetails.tosAccepted.etag === exchangeDetails.tosCurrentEtag) {
+ if (exchange.tosAcceptedTimestamp) {
+ if (exchange.tosAcceptedEtag === exchange.tosCurrentEtag) {
tosAccepted = true;
}
}
@@ -2372,7 +2372,7 @@ export async function internalPrepareCreateWithdrawalGroup(
wgInfo: args.wgInfo,
};
- const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange);
+ const exchangeInfo = await fetchFreshExchange(ws, canonExchange);
const exchangeDetails = exchangeInfo.exchangeDetails;
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
@@ -2515,7 +2515,7 @@ export async function acceptWithdrawalFromUri(
};
}
- await updateExchangeFromUrl(ws, selectedExchange);
+ await fetchFreshExchange(ws, selectedExchange);
const withdrawInfo = await getBankWithdrawalInfo(
ws.http,
req.talerWithdrawUri,