simplify pending transactions, make more tests pass again
This commit is contained in:
parent
7b7e3b4565
commit
8ad36d89f5
@ -217,6 +217,14 @@ export function timestampDifference(t1: Timestamp, t2: Timestamp): Duration {
|
|||||||
return { d_ms: Math.abs(t1.t_ms - t2.t_ms) };
|
return { d_ms: Math.abs(t1.t_ms - t2.t_ms) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function timestampToIsoString(t: Timestamp): string {
|
||||||
|
if (t.t_ms === "never") {
|
||||||
|
return "<never>";
|
||||||
|
} else {
|
||||||
|
return new Date(t.t_ms).toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function timestampIsBetween(
|
export function timestampIsBetween(
|
||||||
t: Timestamp,
|
t: Timestamp,
|
||||||
start: Timestamp,
|
start: Timestamp,
|
||||||
|
@ -167,6 +167,10 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
|
|||||||
merchant,
|
merchant,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// At this point, the original coins should've been refreshed.
|
||||||
|
// It would be too late to refresh them now, as we're past
|
||||||
|
// the two year deposit expiration.
|
||||||
|
|
||||||
await wallet.runUntilDone();
|
await wallet.runUntilDone();
|
||||||
|
|
||||||
const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
|
const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
|
||||||
|
@ -515,25 +515,11 @@ export interface DenominationRecord {
|
|||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ExchangeUpdateStatus {
|
|
||||||
FetchKeys = "fetch-keys",
|
|
||||||
FetchWire = "fetch-wire",
|
|
||||||
FetchTerms = "fetch-terms",
|
|
||||||
FinalizeUpdate = "finalize-update",
|
|
||||||
Finished = "finished",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExchangeBankAccount {
|
export interface ExchangeBankAccount {
|
||||||
payto_uri: string;
|
payto_uri: string;
|
||||||
master_sig: string;
|
master_sig: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ExchangeUpdateReason {
|
|
||||||
Initial = "initial",
|
|
||||||
Forced = "forced",
|
|
||||||
Scheduled = "scheduled",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExchangeDetailsRecord {
|
export interface ExchangeDetailsRecord {
|
||||||
/**
|
/**
|
||||||
* Master public key of the exchange.
|
* Master public key of the exchange.
|
||||||
@ -582,16 +568,6 @@ export interface ExchangeDetailsRecord {
|
|||||||
*/
|
*/
|
||||||
termsOfServiceAcceptedEtag: string | undefined;
|
termsOfServiceAcceptedEtag: string | undefined;
|
||||||
|
|
||||||
/**
|
|
||||||
* Timestamp for last update.
|
|
||||||
*/
|
|
||||||
lastUpdateTime: Timestamp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When should we next update the information about the exchange?
|
|
||||||
*/
|
|
||||||
nextUpdateTime: Timestamp;
|
|
||||||
|
|
||||||
wireInfo: WireInfo;
|
wireInfo: WireInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,27 +605,16 @@ export interface ExchangeRecord {
|
|||||||
permanent: boolean;
|
permanent: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time when the update to the exchange has been started or
|
* Last time when the exchange was updated.
|
||||||
* undefined if no update is in progress.
|
|
||||||
*/
|
*/
|
||||||
updateStarted: Timestamp | undefined;
|
lastUpdate: Timestamp | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of updating the info about the exchange.
|
* Next scheduled update for the exchange.
|
||||||
*
|
*
|
||||||
* FIXME: Adapt this to recent changes regarding how
|
* (This field must always be present, so we can index on the timestamp.)
|
||||||
* updating exchange details works.
|
|
||||||
*/
|
*/
|
||||||
updateStatus: ExchangeUpdateStatus;
|
nextUpdate: Timestamp;
|
||||||
|
|
||||||
updateReason?: ExchangeUpdateReason;
|
|
||||||
|
|
||||||
lastError?: TalerErrorDetails;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry status for fetching updated information about the exchange.
|
|
||||||
*/
|
|
||||||
retryInfo: RetryInfo;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Next time that we should check if coins need to be refreshed.
|
* Next time that we should check if coins need to be refreshed.
|
||||||
@ -657,7 +622,14 @@ export interface ExchangeRecord {
|
|||||||
* Updated whenever the exchange's denominations are updated or when
|
* Updated whenever the exchange's denominations are updated or when
|
||||||
* the refresh check has been done.
|
* the refresh check has been done.
|
||||||
*/
|
*/
|
||||||
nextRefreshCheck?: Timestamp;
|
nextRefreshCheck: Timestamp;
|
||||||
|
|
||||||
|
lastError?: TalerErrorDetails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry status for fetching updated information about the exchange.
|
||||||
|
*/
|
||||||
|
retryInfo: RetryInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +31,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
WalletContractData,
|
WalletContractData,
|
||||||
DenomSelectionState,
|
DenomSelectionState,
|
||||||
ExchangeUpdateStatus,
|
|
||||||
DenominationStatus,
|
DenominationStatus,
|
||||||
CoinSource,
|
CoinSource,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
@ -265,8 +264,9 @@ export async function importBackup(
|
|||||||
},
|
},
|
||||||
permanent: true,
|
permanent: true,
|
||||||
retryInfo: initRetryInfo(false),
|
retryInfo: initRetryInfo(false),
|
||||||
updateStarted: { t_ms: "never" },
|
lastUpdate: undefined,
|
||||||
updateStatus: ExchangeUpdateStatus.Finished,
|
nextUpdate: getTimestampNow(),
|
||||||
|
nextRefreshCheck: getTimestampNow(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,9 +307,7 @@ export async function importBackup(
|
|||||||
auditor_url: x.auditor_url,
|
auditor_url: x.auditor_url,
|
||||||
denomination_keys: x.denomination_keys,
|
denomination_keys: x.denomination_keys,
|
||||||
})),
|
})),
|
||||||
lastUpdateTime: { t_ms: "never" },
|
|
||||||
masterPublicKey: backupExchangeDetails.master_public_key,
|
masterPublicKey: backupExchangeDetails.master_public_key,
|
||||||
nextUpdateTime: { t_ms: "never" },
|
|
||||||
protocolVersion: backupExchangeDetails.protocol_version,
|
protocolVersion: backupExchangeDetails.protocol_version,
|
||||||
reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
|
reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
|
||||||
signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
|
signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
|
||||||
|
@ -42,9 +42,7 @@ import {
|
|||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
DenominationStatus,
|
DenominationStatus,
|
||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
ExchangeUpdateStatus,
|
|
||||||
WireFee,
|
WireFee,
|
||||||
ExchangeUpdateReason,
|
|
||||||
ExchangeDetailsRecord,
|
ExchangeDetailsRecord,
|
||||||
WireInfo,
|
WireInfo,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
@ -299,11 +297,11 @@ async function provideExchangeRecord(
|
|||||||
r = {
|
r = {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
updateStatus: ExchangeUpdateStatus.FetchKeys,
|
|
||||||
updateStarted: now,
|
|
||||||
updateReason: ExchangeUpdateReason.Initial,
|
|
||||||
retryInfo: initRetryInfo(false),
|
retryInfo: initRetryInfo(false),
|
||||||
detailsPointer: undefined,
|
detailsPointer: undefined,
|
||||||
|
lastUpdate: undefined,
|
||||||
|
nextUpdate: now,
|
||||||
|
nextRefreshCheck: now,
|
||||||
};
|
};
|
||||||
await tx.exchanges.put(r);
|
await tx.exchanges.put(r);
|
||||||
}
|
}
|
||||||
@ -411,6 +409,27 @@ async function updateExchangeFromUrlImpl(
|
|||||||
|
|
||||||
const r = await provideExchangeRecord(ws, baseUrl, now);
|
const r = await provideExchangeRecord(ws, baseUrl, now);
|
||||||
|
|
||||||
|
if (!forceNow && r && !isTimestampExpired(r.nextUpdate)) {
|
||||||
|
const res = await ws.db.mktx((x) => ({
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
})).runReadOnly(async (tx) => {
|
||||||
|
const exchange = await tx.exchanges.get(baseUrl);
|
||||||
|
if (!exchange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exchangeDetails = await getExchangeDetails(tx, baseUrl);
|
||||||
|
if (!exchangeDetails) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return { exchange, exchangeDetails };
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
logger.info("using existing exchange info");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("updating exchange /keys info");
|
logger.info("updating exchange /keys info");
|
||||||
|
|
||||||
const timeout = getExchangeRequestTimeout(r);
|
const timeout = getExchangeRequestTimeout(r);
|
||||||
@ -460,11 +479,9 @@ async function updateExchangeFromUrlImpl(
|
|||||||
details = {
|
details = {
|
||||||
auditors: keysInfo.auditors,
|
auditors: keysInfo.auditors,
|
||||||
currency: keysInfo.currency,
|
currency: keysInfo.currency,
|
||||||
lastUpdateTime: now,
|
|
||||||
masterPublicKey: keysInfo.masterPublicKey,
|
masterPublicKey: keysInfo.masterPublicKey,
|
||||||
protocolVersion: keysInfo.protocolVersion,
|
protocolVersion: keysInfo.protocolVersion,
|
||||||
signingKeys: keysInfo.signingKeys,
|
signingKeys: keysInfo.signingKeys,
|
||||||
nextUpdateTime: keysInfo.expiry,
|
|
||||||
reserveClosingDelay: keysInfo.reserveClosingDelay,
|
reserveClosingDelay: keysInfo.reserveClosingDelay,
|
||||||
exchangeBaseUrl: r.baseUrl,
|
exchangeBaseUrl: r.baseUrl,
|
||||||
wireInfo,
|
wireInfo,
|
||||||
@ -472,12 +489,13 @@ async function updateExchangeFromUrlImpl(
|
|||||||
termsOfServiceAcceptedEtag: undefined,
|
termsOfServiceAcceptedEtag: undefined,
|
||||||
termsOfServiceLastEtag: tosDownload.tosEtag,
|
termsOfServiceLastEtag: tosDownload.tosEtag,
|
||||||
};
|
};
|
||||||
r.updateStatus = ExchangeUpdateStatus.FetchWire;
|
|
||||||
// FIXME: only update if pointer got updated
|
// FIXME: only update if pointer got updated
|
||||||
r.lastError = undefined;
|
r.lastError = undefined;
|
||||||
r.retryInfo = initRetryInfo(false);
|
r.retryInfo = initRetryInfo(false);
|
||||||
|
r.lastUpdate = getTimestampNow();
|
||||||
|
r.nextUpdate = keysInfo.expiry,
|
||||||
// New denominations might be available.
|
// New denominations might be available.
|
||||||
r.nextRefreshCheck = undefined;
|
r.nextRefreshCheck = getTimestampNow();
|
||||||
r.detailsPointer = {
|
r.detailsPointer = {
|
||||||
currency: details.currency,
|
currency: details.currency,
|
||||||
masterPublicKey: details.masterPublicKey,
|
masterPublicKey: details.masterPublicKey,
|
||||||
|
@ -468,7 +468,7 @@ async function recordConfirmPay(
|
|||||||
const p = await tx.proposals.get(proposal.proposalId);
|
const p = await tx.proposals.get(proposal.proposalId);
|
||||||
if (p) {
|
if (p) {
|
||||||
p.proposalStatus = ProposalStatus.ACCEPTED;
|
p.proposalStatus = ProposalStatus.ACCEPTED;
|
||||||
p.lastError = undefined;
|
delete p.lastError;
|
||||||
p.retryInfo = initRetryInfo(false);
|
p.retryInfo = initRetryInfo(false);
|
||||||
await tx.proposals.put(p);
|
await tx.proposals.put(p);
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
ExchangeUpdateStatus,
|
|
||||||
ProposalStatus,
|
ProposalStatus,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
AbortStatus,
|
AbortStatus,
|
||||||
@ -27,31 +26,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
PendingOperationType,
|
PendingOperationType,
|
||||||
ExchangeUpdateOperationStage,
|
|
||||||
ReserveType,
|
ReserveType,
|
||||||
} from "../pending-types";
|
} from "../pending-types";
|
||||||
import {
|
import { getTimestampNow, Timestamp } from "@gnu-taler/taler-util";
|
||||||
Duration,
|
|
||||||
getTimestampNow,
|
|
||||||
Timestamp,
|
|
||||||
getDurationRemaining,
|
|
||||||
durationMin,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { getBalancesInsideTransaction } from "./balance";
|
import { getBalancesInsideTransaction } from "./balance";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
|
||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
|
|
||||||
function updateRetryDelay(
|
|
||||||
oldDelay: Duration,
|
|
||||||
now: Timestamp,
|
|
||||||
retryTimestamp: Timestamp,
|
|
||||||
): Duration {
|
|
||||||
const remaining = getDurationRemaining(retryTimestamp, now);
|
|
||||||
const nextDelay = durationMin(oldDelay, remaining);
|
|
||||||
return nextDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function gatherExchangePending(
|
async function gatherExchangePending(
|
||||||
tx: GetReadOnlyAccess<{
|
tx: GetReadOnlyAccess<{
|
||||||
exchanges: typeof WalletStoresV1.exchanges;
|
exchanges: typeof WalletStoresV1.exchanges;
|
||||||
@ -59,97 +40,22 @@ async function gatherExchangePending(
|
|||||||
}>,
|
}>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.exchanges.iter().forEachAsync(async (e) => {
|
await tx.exchanges.iter().forEachAsync(async (e) => {
|
||||||
switch (e.updateStatus) {
|
resp.pendingOperations.push({
|
||||||
case ExchangeUpdateStatus.Finished:
|
type: PendingOperationType.ExchangeUpdate,
|
||||||
if (e.lastError) {
|
givesLifeness: false,
|
||||||
resp.pendingOperations.push({
|
timestampDue: e.nextUpdate,
|
||||||
type: PendingOperationType.Bug,
|
exchangeBaseUrl: e.baseUrl,
|
||||||
givesLifeness: false,
|
lastError: e.lastError,
|
||||||
message:
|
});
|
||||||
"Exchange record is in FINISHED state but has lastError set",
|
|
||||||
details: {
|
resp.pendingOperations.push({
|
||||||
exchangeBaseUrl: e.baseUrl,
|
type: PendingOperationType.ExchangeCheckRefresh,
|
||||||
},
|
timestampDue: e.nextRefreshCheck,
|
||||||
});
|
givesLifeness: false,
|
||||||
}
|
exchangeBaseUrl: e.baseUrl,
|
||||||
const details = await getExchangeDetails(tx, e.baseUrl);
|
});
|
||||||
const keysUpdateRequired =
|
|
||||||
details && details.nextUpdateTime.t_ms < now.t_ms;
|
|
||||||
if (keysUpdateRequired) {
|
|
||||||
resp.pendingOperations.push({
|
|
||||||
type: PendingOperationType.ExchangeUpdate,
|
|
||||||
givesLifeness: false,
|
|
||||||
stage: ExchangeUpdateOperationStage.FetchKeys,
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
lastError: e.lastError,
|
|
||||||
reason: "scheduled",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
details &&
|
|
||||||
(!e.nextRefreshCheck || e.nextRefreshCheck.t_ms < now.t_ms)
|
|
||||||
) {
|
|
||||||
resp.pendingOperations.push({
|
|
||||||
type: PendingOperationType.ExchangeCheckRefresh,
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
givesLifeness: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ExchangeUpdateStatus.FetchKeys:
|
|
||||||
if (onlyDue && e.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resp.pendingOperations.push({
|
|
||||||
type: PendingOperationType.ExchangeUpdate,
|
|
||||||
givesLifeness: false,
|
|
||||||
stage: ExchangeUpdateOperationStage.FetchKeys,
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
lastError: e.lastError,
|
|
||||||
reason: e.updateReason || "unknown",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case ExchangeUpdateStatus.FetchWire:
|
|
||||||
if (onlyDue && e.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resp.pendingOperations.push({
|
|
||||||
type: PendingOperationType.ExchangeUpdate,
|
|
||||||
givesLifeness: false,
|
|
||||||
stage: ExchangeUpdateOperationStage.FetchWire,
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
lastError: e.lastError,
|
|
||||||
reason: e.updateReason || "unknown",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case ExchangeUpdateStatus.FinalizeUpdate:
|
|
||||||
if (onlyDue && e.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resp.pendingOperations.push({
|
|
||||||
type: PendingOperationType.ExchangeUpdate,
|
|
||||||
givesLifeness: false,
|
|
||||||
stage: ExchangeUpdateOperationStage.FinalizeUpdate,
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
lastError: e.lastError,
|
|
||||||
reason: e.updateReason || "unknown",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
resp.pendingOperations.push({
|
|
||||||
type: PendingOperationType.Bug,
|
|
||||||
givesLifeness: false,
|
|
||||||
message: "Unknown exchangeUpdateStatus",
|
|
||||||
details: {
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
exchangeUpdateStatus: e.updateStatus,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,16 +63,11 @@ async function gatherReservePending(
|
|||||||
tx: GetReadOnlyAccess<{ reserves: typeof WalletStoresV1.reserves }>,
|
tx: GetReadOnlyAccess<{ reserves: typeof WalletStoresV1.reserves }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// FIXME: this should be optimized by using an index for "onlyDue==true".
|
|
||||||
await tx.reserves.iter().forEach((reserve) => {
|
await tx.reserves.iter().forEach((reserve) => {
|
||||||
const reserveType = reserve.bankInfo
|
const reserveType = reserve.bankInfo
|
||||||
? ReserveType.TalerBankWithdraw
|
? ReserveType.TalerBankWithdraw
|
||||||
: ReserveType.Manual;
|
: ReserveType.Manual;
|
||||||
if (!reserve.retryInfo.active) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (reserve.reserveStatus) {
|
switch (reserve.reserveStatus) {
|
||||||
case ReserveRecordStatus.DORMANT:
|
case ReserveRecordStatus.DORMANT:
|
||||||
// nothing to report as pending
|
// nothing to report as pending
|
||||||
@ -174,17 +75,10 @@ async function gatherReservePending(
|
|||||||
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
||||||
case ReserveRecordStatus.QUERYING_STATUS:
|
case ReserveRecordStatus.QUERYING_STATUS:
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
|
||||||
resp.nextRetryDelay,
|
|
||||||
now,
|
|
||||||
reserve.retryInfo.nextRetry,
|
|
||||||
);
|
|
||||||
if (onlyDue && reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.Reserve,
|
type: PendingOperationType.Reserve,
|
||||||
givesLifeness: true,
|
givesLifeness: true,
|
||||||
|
timestampDue: reserve.retryInfo.nextRetry,
|
||||||
stage: reserve.reserveStatus,
|
stage: reserve.reserveStatus,
|
||||||
timestampCreated: reserve.timestampCreated,
|
timestampCreated: reserve.timestampCreated,
|
||||||
reserveType,
|
reserveType,
|
||||||
@ -193,15 +87,7 @@ async function gatherReservePending(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
resp.pendingOperations.push({
|
// FIXME: report problem!
|
||||||
type: PendingOperationType.Bug,
|
|
||||||
givesLifeness: false,
|
|
||||||
message: "Unknown reserve record status",
|
|
||||||
details: {
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
reserveStatus: reserve.reserveStatus,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -211,24 +97,15 @@ async function gatherRefreshPending(
|
|||||||
tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups }>,
|
tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.refreshGroups.iter().forEach((r) => {
|
await tx.refreshGroups.iter().forEach((r) => {
|
||||||
if (r.timestampFinished) {
|
if (r.timestampFinished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
|
||||||
resp.nextRetryDelay,
|
|
||||||
now,
|
|
||||||
r.retryInfo.nextRetry,
|
|
||||||
);
|
|
||||||
if (onlyDue && r.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.Refresh,
|
type: PendingOperationType.Refresh,
|
||||||
givesLifeness: true,
|
givesLifeness: true,
|
||||||
|
timestampDue: r.retryInfo.nextRetry,
|
||||||
refreshGroupId: r.refreshGroupId,
|
refreshGroupId: r.refreshGroupId,
|
||||||
finishedPerCoin: r.finishedPerCoin,
|
finishedPerCoin: r.finishedPerCoin,
|
||||||
retryInfo: r.retryInfo,
|
retryInfo: r.retryInfo,
|
||||||
@ -243,20 +120,11 @@ async function gatherWithdrawalPending(
|
|||||||
}>,
|
}>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
|
await tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
|
||||||
if (wsr.timestampFinish) {
|
if (wsr.timestampFinish) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
|
||||||
resp.nextRetryDelay,
|
|
||||||
now,
|
|
||||||
wsr.retryInfo.nextRetry,
|
|
||||||
);
|
|
||||||
if (onlyDue && wsr.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let numCoinsWithdrawn = 0;
|
let numCoinsWithdrawn = 0;
|
||||||
let numCoinsTotal = 0;
|
let numCoinsTotal = 0;
|
||||||
await tx.planchets.indexes.byGroup
|
await tx.planchets.indexes.byGroup
|
||||||
@ -270,8 +138,7 @@ async function gatherWithdrawalPending(
|
|||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.Withdraw,
|
type: PendingOperationType.Withdraw,
|
||||||
givesLifeness: true,
|
givesLifeness: true,
|
||||||
numCoinsTotal,
|
timestampDue: wsr.retryInfo.nextRetry,
|
||||||
numCoinsWithdrawn,
|
|
||||||
withdrawalGroupId: wsr.withdrawalGroupId,
|
withdrawalGroupId: wsr.withdrawalGroupId,
|
||||||
lastError: wsr.lastError,
|
lastError: wsr.lastError,
|
||||||
retryInfo: wsr.retryInfo,
|
retryInfo: wsr.retryInfo,
|
||||||
@ -283,42 +150,15 @@ async function gatherProposalPending(
|
|||||||
tx: GetReadOnlyAccess<{ proposals: typeof WalletStoresV1.proposals }>,
|
tx: GetReadOnlyAccess<{ proposals: typeof WalletStoresV1.proposals }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.proposals.iter().forEach((proposal) => {
|
await tx.proposals.iter().forEach((proposal) => {
|
||||||
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
|
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
|
||||||
if (onlyDue) {
|
// Nothing to do, user needs to choose.
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dl = proposal.download;
|
|
||||||
if (!dl) {
|
|
||||||
resp.pendingOperations.push({
|
|
||||||
type: PendingOperationType.Bug,
|
|
||||||
message: "proposal is in invalid state",
|
|
||||||
details: {},
|
|
||||||
givesLifeness: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resp.pendingOperations.push({
|
|
||||||
type: PendingOperationType.ProposalChoice,
|
|
||||||
givesLifeness: false,
|
|
||||||
merchantBaseUrl: dl.contractData.merchantBaseUrl,
|
|
||||||
proposalId: proposal.proposalId,
|
|
||||||
proposalTimestamp: proposal.timestamp,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
|
} else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
|
||||||
resp.nextRetryDelay,
|
|
||||||
now,
|
|
||||||
proposal.retryInfo.nextRetry,
|
|
||||||
);
|
|
||||||
if (onlyDue && proposal.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.ProposalDownload,
|
type: PendingOperationType.ProposalDownload,
|
||||||
givesLifeness: true,
|
givesLifeness: true,
|
||||||
|
timestampDue: proposal.retryInfo.nextRetry,
|
||||||
merchantBaseUrl: proposal.merchantBaseUrl,
|
merchantBaseUrl: proposal.merchantBaseUrl,
|
||||||
orderId: proposal.orderId,
|
orderId: proposal.orderId,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
@ -334,24 +174,16 @@ async function gatherTipPending(
|
|||||||
tx: GetReadOnlyAccess<{ tips: typeof WalletStoresV1.tips }>,
|
tx: GetReadOnlyAccess<{ tips: typeof WalletStoresV1.tips }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.tips.iter().forEach((tip) => {
|
await tx.tips.iter().forEach((tip) => {
|
||||||
if (tip.pickedUpTimestamp) {
|
if (tip.pickedUpTimestamp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
|
||||||
resp.nextRetryDelay,
|
|
||||||
now,
|
|
||||||
tip.retryInfo.nextRetry,
|
|
||||||
);
|
|
||||||
if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (tip.acceptedTimestamp) {
|
if (tip.acceptedTimestamp) {
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.TipPickup,
|
type: PendingOperationType.TipPickup,
|
||||||
givesLifeness: true,
|
givesLifeness: true,
|
||||||
|
timestampDue: tip.retryInfo.nextRetry,
|
||||||
merchantBaseUrl: tip.merchantBaseUrl,
|
merchantBaseUrl: tip.merchantBaseUrl,
|
||||||
tipId: tip.walletTipId,
|
tipId: tip.walletTipId,
|
||||||
merchantTipId: tip.merchantTipId,
|
merchantTipId: tip.merchantTipId,
|
||||||
@ -364,41 +196,28 @@ async function gatherPurchasePending(
|
|||||||
tx: GetReadOnlyAccess<{ purchases: typeof WalletStoresV1.purchases }>,
|
tx: GetReadOnlyAccess<{ purchases: typeof WalletStoresV1.purchases }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.purchases.iter().forEach((pr) => {
|
await tx.purchases.iter().forEach((pr) => {
|
||||||
if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) {
|
if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) {
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
resp.pendingOperations.push({
|
||||||
resp.nextRetryDelay,
|
type: PendingOperationType.Pay,
|
||||||
now,
|
givesLifeness: true,
|
||||||
pr.payRetryInfo.nextRetry,
|
timestampDue: pr.payRetryInfo.nextRetry,
|
||||||
);
|
isReplay: false,
|
||||||
if (!onlyDue || pr.payRetryInfo.nextRetry.t_ms <= now.t_ms) {
|
proposalId: pr.proposalId,
|
||||||
resp.pendingOperations.push({
|
retryInfo: pr.payRetryInfo,
|
||||||
type: PendingOperationType.Pay,
|
lastError: pr.lastPayError,
|
||||||
givesLifeness: true,
|
});
|
||||||
isReplay: false,
|
|
||||||
proposalId: pr.proposalId,
|
|
||||||
retryInfo: pr.payRetryInfo,
|
|
||||||
lastError: pr.lastPayError,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (pr.refundQueryRequested) {
|
if (pr.refundQueryRequested) {
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
resp.pendingOperations.push({
|
||||||
resp.nextRetryDelay,
|
type: PendingOperationType.RefundQuery,
|
||||||
now,
|
givesLifeness: true,
|
||||||
pr.refundStatusRetryInfo.nextRetry,
|
timestampDue: pr.refundStatusRetryInfo.nextRetry,
|
||||||
);
|
proposalId: pr.proposalId,
|
||||||
if (!onlyDue || pr.refundStatusRetryInfo.nextRetry.t_ms <= now.t_ms) {
|
retryInfo: pr.refundStatusRetryInfo,
|
||||||
resp.pendingOperations.push({
|
lastError: pr.lastRefundStatusError,
|
||||||
type: PendingOperationType.RefundQuery,
|
});
|
||||||
givesLifeness: true,
|
|
||||||
proposalId: pr.proposalId,
|
|
||||||
retryInfo: pr.refundStatusRetryInfo,
|
|
||||||
lastError: pr.lastRefundStatusError,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -407,23 +226,15 @@ async function gatherRecoupPending(
|
|||||||
tx: GetReadOnlyAccess<{ recoupGroups: typeof WalletStoresV1.recoupGroups }>,
|
tx: GetReadOnlyAccess<{ recoupGroups: typeof WalletStoresV1.recoupGroups }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.recoupGroups.iter().forEach((rg) => {
|
await tx.recoupGroups.iter().forEach((rg) => {
|
||||||
if (rg.timestampFinished) {
|
if (rg.timestampFinished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
|
||||||
resp.nextRetryDelay,
|
|
||||||
now,
|
|
||||||
rg.retryInfo.nextRetry,
|
|
||||||
);
|
|
||||||
if (onlyDue && rg.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.Recoup,
|
type: PendingOperationType.Recoup,
|
||||||
givesLifeness: true,
|
givesLifeness: true,
|
||||||
|
timestampDue: rg.retryInfo.nextRetry,
|
||||||
recoupGroupId: rg.recoupGroupId,
|
recoupGroupId: rg.recoupGroupId,
|
||||||
retryInfo: rg.retryInfo,
|
retryInfo: rg.retryInfo,
|
||||||
lastError: rg.lastError,
|
lastError: rg.lastError,
|
||||||
@ -435,23 +246,15 @@ async function gatherDepositPending(
|
|||||||
tx: GetReadOnlyAccess<{ depositGroups: typeof WalletStoresV1.depositGroups }>,
|
tx: GetReadOnlyAccess<{ depositGroups: typeof WalletStoresV1.depositGroups }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.depositGroups.iter().forEach((dg) => {
|
await tx.depositGroups.iter().forEach((dg) => {
|
||||||
if (dg.timestampFinished) {
|
if (dg.timestampFinished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
|
||||||
resp.nextRetryDelay,
|
|
||||||
now,
|
|
||||||
dg.retryInfo.nextRetry,
|
|
||||||
);
|
|
||||||
if (onlyDue && dg.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.Deposit,
|
type: PendingOperationType.Deposit,
|
||||||
givesLifeness: true,
|
givesLifeness: true,
|
||||||
|
timestampDue: dg.retryInfo.nextRetry,
|
||||||
depositGroupId: dg.depositGroupId,
|
depositGroupId: dg.depositGroupId,
|
||||||
retryInfo: dg.retryInfo,
|
retryInfo: dg.retryInfo,
|
||||||
lastError: dg.lastError,
|
lastError: dg.lastError,
|
||||||
@ -461,7 +264,6 @@ async function gatherDepositPending(
|
|||||||
|
|
||||||
export async function getPendingOperations(
|
export async function getPendingOperations(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
{ onlyDue = false } = {},
|
|
||||||
): Promise<PendingOperationsResponse> {
|
): Promise<PendingOperationsResponse> {
|
||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
return await ws.db
|
return await ws.db
|
||||||
@ -482,20 +284,18 @@ export async function getPendingOperations(
|
|||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const walletBalance = await getBalancesInsideTransaction(ws, tx);
|
const walletBalance = await getBalancesInsideTransaction(ws, tx);
|
||||||
const resp: PendingOperationsResponse = {
|
const resp: PendingOperationsResponse = {
|
||||||
nextRetryDelay: { d_ms: Number.MAX_SAFE_INTEGER },
|
|
||||||
onlyDue: onlyDue,
|
|
||||||
walletBalance,
|
walletBalance,
|
||||||
pendingOperations: [],
|
pendingOperations: [],
|
||||||
};
|
};
|
||||||
await gatherExchangePending(tx, now, resp, onlyDue);
|
await gatherExchangePending(tx, now, resp);
|
||||||
await gatherReservePending(tx, now, resp, onlyDue);
|
await gatherReservePending(tx, now, resp);
|
||||||
await gatherRefreshPending(tx, now, resp, onlyDue);
|
await gatherRefreshPending(tx, now, resp);
|
||||||
await gatherWithdrawalPending(tx, now, resp, onlyDue);
|
await gatherWithdrawalPending(tx, now, resp);
|
||||||
await gatherProposalPending(tx, now, resp, onlyDue);
|
await gatherProposalPending(tx, now, resp);
|
||||||
await gatherTipPending(tx, now, resp, onlyDue);
|
await gatherTipPending(tx, now, resp);
|
||||||
await gatherPurchasePending(tx, now, resp, onlyDue);
|
await gatherPurchasePending(tx, now, resp);
|
||||||
await gatherRecoupPending(tx, now, resp, onlyDue);
|
await gatherRecoupPending(tx, now, resp);
|
||||||
await gatherDepositPending(tx, now, resp, onlyDue);
|
await gatherDepositPending(tx, now, resp);
|
||||||
return resp;
|
return resp;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
RefreshGroupId,
|
RefreshGroupId,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
TalerErrorDetails,
|
TalerErrorDetails,
|
||||||
|
timestampToIsoString,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
|
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
|
||||||
import { amountToPretty } from "@gnu-taler/taler-util";
|
import { amountToPretty } from "@gnu-taler/taler-util";
|
||||||
@ -864,7 +865,12 @@ export async function autoRefresh(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
logger.info(`doing auto-refresh check for '${exchangeBaseUrl}'`);
|
||||||
await updateExchangeFromUrl(ws, exchangeBaseUrl, true);
|
await updateExchangeFromUrl(ws, exchangeBaseUrl, true);
|
||||||
|
let minCheckThreshold = timestampAddDuration(
|
||||||
|
getTimestampNow(),
|
||||||
|
durationFromSpec({ days: 1 }),
|
||||||
|
);
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
coins: x.coins,
|
coins: x.coins,
|
||||||
@ -899,28 +905,20 @@ export async function autoRefresh(
|
|||||||
const executeThreshold = getAutoRefreshExecuteThreshold(denom);
|
const executeThreshold = getAutoRefreshExecuteThreshold(denom);
|
||||||
if (isTimestampExpired(executeThreshold)) {
|
if (isTimestampExpired(executeThreshold)) {
|
||||||
refreshCoins.push(coin);
|
refreshCoins.push(coin);
|
||||||
|
} else {
|
||||||
|
const checkThreshold = getAutoRefreshCheckThreshold(denom);
|
||||||
|
minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (refreshCoins.length > 0) {
|
if (refreshCoins.length > 0) {
|
||||||
await createRefreshGroup(ws, tx, refreshCoins, RefreshReason.Scheduled);
|
await createRefreshGroup(ws, tx, refreshCoins, RefreshReason.Scheduled);
|
||||||
}
|
}
|
||||||
|
logger.info(
|
||||||
const denoms = await tx.denominations.indexes.byExchangeBaseUrl
|
`current wallet time: ${timestampToIsoString(getTimestampNow())}`,
|
||||||
.iter(exchangeBaseUrl)
|
);
|
||||||
.toArray();
|
logger.info(
|
||||||
let minCheckThreshold = timestampAddDuration(
|
`next refresh check at ${timestampToIsoString(minCheckThreshold)}`,
|
||||||
getTimestampNow(),
|
|
||||||
durationFromSpec({ days: 1 }),
|
|
||||||
);
|
);
|
||||||
for (const denom of denoms) {
|
|
||||||
const checkThreshold = getAutoRefreshCheckThreshold(denom);
|
|
||||||
const executeThreshold = getAutoRefreshExecuteThreshold(denom);
|
|
||||||
if (isTimestampExpired(executeThreshold)) {
|
|
||||||
// No need to consider this denomination, we already did an auto refresh check.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold);
|
|
||||||
}
|
|
||||||
exchange.nextRefreshCheck = minCheckThreshold;
|
exchange.nextRefreshCheck = minCheckThreshold;
|
||||||
await tx.exchanges.put(exchange);
|
await tx.exchanges.put(exchange);
|
||||||
});
|
});
|
||||||
|
@ -34,7 +34,6 @@ import { ReserveRecordStatus } from "./db.js";
|
|||||||
import { RetryInfo } from "./util/retries.js";
|
import { RetryInfo } from "./util/retries.js";
|
||||||
|
|
||||||
export enum PendingOperationType {
|
export enum PendingOperationType {
|
||||||
Bug = "bug",
|
|
||||||
ExchangeUpdate = "exchange-update",
|
ExchangeUpdate = "exchange-update",
|
||||||
ExchangeCheckRefresh = "exchange-check-refresh",
|
ExchangeCheckRefresh = "exchange-check-refresh",
|
||||||
Pay = "pay",
|
Pay = "pay",
|
||||||
@ -44,7 +43,6 @@ export enum PendingOperationType {
|
|||||||
Reserve = "reserve",
|
Reserve = "reserve",
|
||||||
Recoup = "recoup",
|
Recoup = "recoup",
|
||||||
RefundQuery = "refund-query",
|
RefundQuery = "refund-query",
|
||||||
TipChoice = "tip-choice",
|
|
||||||
TipPickup = "tip-pickup",
|
TipPickup = "tip-pickup",
|
||||||
Withdraw = "withdraw",
|
Withdraw = "withdraw",
|
||||||
Deposit = "deposit",
|
Deposit = "deposit",
|
||||||
@ -55,16 +53,13 @@ export enum PendingOperationType {
|
|||||||
*/
|
*/
|
||||||
export type PendingOperationInfo = PendingOperationInfoCommon &
|
export type PendingOperationInfo = PendingOperationInfoCommon &
|
||||||
(
|
(
|
||||||
| PendingBugOperation
|
|
||||||
| PendingExchangeUpdateOperation
|
| PendingExchangeUpdateOperation
|
||||||
| PendingExchangeCheckRefreshOperation
|
| PendingExchangeCheckRefreshOperation
|
||||||
| PendingPayOperation
|
| PendingPayOperation
|
||||||
| PendingProposalChoiceOperation
|
|
||||||
| PendingProposalDownloadOperation
|
| PendingProposalDownloadOperation
|
||||||
| PendingRefreshOperation
|
| PendingRefreshOperation
|
||||||
| PendingRefundQueryOperation
|
| PendingRefundQueryOperation
|
||||||
| PendingReserveOperation
|
| PendingReserveOperation
|
||||||
| PendingTipChoiceOperation
|
|
||||||
| PendingTipPickupOperation
|
| PendingTipPickupOperation
|
||||||
| PendingWithdrawOperation
|
| PendingWithdrawOperation
|
||||||
| PendingRecoupOperation
|
| PendingRecoupOperation
|
||||||
@ -76,8 +71,6 @@ export type PendingOperationInfo = PendingOperationInfoCommon &
|
|||||||
*/
|
*/
|
||||||
export interface PendingExchangeUpdateOperation {
|
export interface PendingExchangeUpdateOperation {
|
||||||
type: PendingOperationType.ExchangeUpdate;
|
type: PendingOperationType.ExchangeUpdate;
|
||||||
stage: ExchangeUpdateOperationStage;
|
|
||||||
reason: string;
|
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
lastError: TalerErrorDetails | undefined;
|
lastError: TalerErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
@ -91,26 +84,6 @@ export interface PendingExchangeCheckRefreshOperation {
|
|||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Some internal 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 {
|
export enum ReserveType {
|
||||||
/**
|
/**
|
||||||
* Manually created.
|
* Manually created.
|
||||||
@ -183,17 +156,6 @@ export interface PendingTipPickupOperation {
|
|||||||
merchantTipId: 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 wallet is signing coins and then sending them to
|
||||||
* the merchant.
|
* the merchant.
|
||||||
@ -232,8 +194,6 @@ export interface PendingWithdrawOperation {
|
|||||||
lastError: TalerErrorDetails | undefined;
|
lastError: TalerErrorDetails | undefined;
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
withdrawalGroupId: string;
|
withdrawalGroupId: string;
|
||||||
numCoinsWithdrawn: number;
|
|
||||||
numCoinsTotal: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -257,13 +217,18 @@ export interface PendingOperationInfoCommon {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set to true if the operation indicates that something is really in progress,
|
* Set to true if the operation indicates that something is really in progress,
|
||||||
* as opposed to some regular scheduled operation or a permanent failure.
|
* as opposed to some regular scheduled operation that can be tried later.
|
||||||
*/
|
*/
|
||||||
givesLifeness: boolean;
|
givesLifeness: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retry info, not available on all pending operations.
|
* Timestamp when the pending operation should be executed next.
|
||||||
* If it is available, it must have the same name.
|
*/
|
||||||
|
timestampDue: Timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry info. Currently used to stop the wallet after any operation
|
||||||
|
* exceeds a number of retries.
|
||||||
*/
|
*/
|
||||||
retryInfo?: RetryInfo;
|
retryInfo?: RetryInfo;
|
||||||
}
|
}
|
||||||
@ -281,15 +246,4 @@ export interface PendingOperationsResponse {
|
|||||||
* Current wallet balance, including pending balances.
|
* Current wallet balance, including pending balances.
|
||||||
*/
|
*/
|
||||||
walletBalance: BalancesResponse;
|
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;
|
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,6 @@ export namespace ContractTermsUtil {
|
|||||||
* to forgettable fields and other restrictions for forgettable JSON.
|
* to forgettable fields and other restrictions for forgettable JSON.
|
||||||
*/
|
*/
|
||||||
export function validateForgettable(anyJson: any): boolean {
|
export function validateForgettable(anyJson: any): boolean {
|
||||||
console.warn("calling validateForgettable", anyJson);
|
|
||||||
if (typeof anyJson === "string") {
|
if (typeof anyJson === "string") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -206,7 +205,6 @@ export namespace ContractTermsUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("invalid type");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,10 +81,11 @@ export function initRetryInfo(
|
|||||||
retryCounter: 0,
|
retryCounter: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const now = getTimestampNow();
|
||||||
const info = {
|
const info = {
|
||||||
firstTry: getTimestampNow(),
|
firstTry: now,
|
||||||
active: true,
|
active: true,
|
||||||
nextRetry: { t_ms: 0 },
|
nextRetry: now,
|
||||||
retryCounter: 0,
|
retryCounter: 0,
|
||||||
};
|
};
|
||||||
updateRetryInfoTimeout(info, p);
|
updateRetryInfoTimeout(info, p);
|
||||||
|
@ -27,7 +27,15 @@ import {
|
|||||||
codecForAny,
|
codecForAny,
|
||||||
codecForDeleteTransactionRequest,
|
codecForDeleteTransactionRequest,
|
||||||
DeleteTransactionRequest,
|
DeleteTransactionRequest,
|
||||||
|
durationFromSpec,
|
||||||
|
durationMax,
|
||||||
|
durationMin,
|
||||||
|
getDurationRemaining,
|
||||||
|
isTimestampExpired,
|
||||||
|
j2s,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
|
Timestamp,
|
||||||
|
timestampMin,
|
||||||
WalletCurrencyInfo,
|
WalletCurrencyInfo,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
|
import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
|
||||||
@ -105,11 +113,8 @@ import {
|
|||||||
AuditorTrustRecord,
|
AuditorTrustRecord,
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
DenominationRecord,
|
|
||||||
ExchangeDetailsRecord,
|
ExchangeDetailsRecord,
|
||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
PurchaseRecord,
|
|
||||||
RefundState,
|
|
||||||
ReserveRecord,
|
ReserveRecord,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
@ -164,7 +169,6 @@ import {
|
|||||||
ManualWithdrawalDetails,
|
ManualWithdrawalDetails,
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
PrepareTipResult,
|
PrepareTipResult,
|
||||||
PurchaseDetails,
|
|
||||||
RecoveryLoadRequest,
|
RecoveryLoadRequest,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
ReturnCoinsRequest,
|
ReturnCoinsRequest,
|
||||||
@ -180,7 +184,6 @@ import { AsyncOpMemoSingle } from "./util/asyncMemo";
|
|||||||
import { HttpRequestLibrary } from "./util/http";
|
import { HttpRequestLibrary } from "./util/http";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { AsyncCondition } from "./util/promiseUtils";
|
import { AsyncCondition } from "./util/promiseUtils";
|
||||||
import { Duration, durationMin } from "@gnu-taler/taler-util";
|
|
||||||
import { TimerGroup } from "./util/timer";
|
import { TimerGroup } from "./util/timer";
|
||||||
import { getExchangeTrust } from "./operations/currencies.js";
|
import { getExchangeTrust } from "./operations/currencies.js";
|
||||||
import { DbAccess } from "./util/query.js";
|
import { DbAccess } from "./util/query.js";
|
||||||
@ -261,9 +264,6 @@ export class Wallet {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
|
logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
|
||||||
switch (pending.type) {
|
switch (pending.type) {
|
||||||
case PendingOperationType.Bug:
|
|
||||||
// Nothing to do, will just be displayed to the user
|
|
||||||
return;
|
|
||||||
case PendingOperationType.ExchangeUpdate:
|
case PendingOperationType.ExchangeUpdate:
|
||||||
await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
|
await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
|
||||||
break;
|
break;
|
||||||
@ -280,15 +280,9 @@ export class Wallet {
|
|||||||
forceNow,
|
forceNow,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case PendingOperationType.ProposalChoice:
|
|
||||||
// Nothing to do, user needs to accept/reject
|
|
||||||
break;
|
|
||||||
case PendingOperationType.ProposalDownload:
|
case PendingOperationType.ProposalDownload:
|
||||||
await processDownloadProposal(this.ws, pending.proposalId, forceNow);
|
await processDownloadProposal(this.ws, pending.proposalId, forceNow);
|
||||||
break;
|
break;
|
||||||
case PendingOperationType.TipChoice:
|
|
||||||
// Nothing to do, user needs to accept/reject
|
|
||||||
break;
|
|
||||||
case PendingOperationType.TipPickup:
|
case PendingOperationType.TipPickup:
|
||||||
await processTip(this.ws, pending.tipId, forceNow);
|
await processTip(this.ws, pending.tipId, forceNow);
|
||||||
break;
|
break;
|
||||||
@ -316,9 +310,11 @@ export class Wallet {
|
|||||||
* Process pending operations.
|
* Process pending operations.
|
||||||
*/
|
*/
|
||||||
public async runPending(forceNow = false): Promise<void> {
|
public async runPending(forceNow = false): Promise<void> {
|
||||||
const onlyDue = !forceNow;
|
const pendingOpsResponse = await this.getPendingOperations();
|
||||||
const pendingOpsResponse = await this.getPendingOperations({ onlyDue });
|
|
||||||
for (const p of pendingOpsResponse.pendingOperations) {
|
for (const p of pendingOpsResponse.pendingOperations) {
|
||||||
|
if (!forceNow && !isTimestampExpired(p.timestampDue)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await this.processOnePendingOperation(p, forceNow);
|
await this.processOnePendingOperation(p, forceNow);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -364,7 +360,7 @@ export class Wallet {
|
|||||||
if (!maxRetries) {
|
if (!maxRetries) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.getPendingOperations({ onlyDue: false })
|
this.getPendingOperations()
|
||||||
.then((pending) => {
|
.then((pending) => {
|
||||||
for (const p of pending.pendingOperations) {
|
for (const p of pending.pendingOperations) {
|
||||||
if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
|
if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
|
||||||
@ -408,51 +404,53 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async runRetryLoopImpl(): Promise<void> {
|
private async runRetryLoopImpl(): Promise<void> {
|
||||||
let iteration = 0;
|
for (let iteration = 0; !this.stopped; iteration++) {
|
||||||
for (; !this.stopped; iteration++) {
|
const pending = await this.getPendingOperations();
|
||||||
const pending = await this.getPendingOperations({ onlyDue: true });
|
logger.trace(`pending operations: ${j2s(pending)}`);
|
||||||
let numDueAndLive = 0;
|
let numGivingLiveness = 0;
|
||||||
|
let numDue = 0;
|
||||||
|
let minDue: Timestamp = { t_ms: "never" };
|
||||||
for (const p of pending.pendingOperations) {
|
for (const p of pending.pendingOperations) {
|
||||||
|
minDue = timestampMin(minDue, p.timestampDue);
|
||||||
|
if (isTimestampExpired(p.timestampDue)) {
|
||||||
|
numDue++;
|
||||||
|
}
|
||||||
if (p.givesLifeness) {
|
if (p.givesLifeness) {
|
||||||
numDueAndLive++;
|
numGivingLiveness++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Make sure that we run tasks that don't give lifeness at least
|
// Make sure that we run tasks that don't give lifeness at least
|
||||||
// one time.
|
// one time.
|
||||||
if (iteration !== 0 && numDueAndLive === 0) {
|
if (iteration !== 0 && numDue === 0) {
|
||||||
const allPending = await this.getPendingOperations({ onlyDue: false });
|
// We've executed pending, due operations at least one.
|
||||||
let numPending = 0;
|
// Now we don't have any more operations available,
|
||||||
let numGivingLiveness = 0;
|
// and need to wait.
|
||||||
for (const p of allPending.pendingOperations) {
|
|
||||||
numPending++;
|
// Wait for at most 5 seconds to the next check.
|
||||||
if (p.givesLifeness) {
|
const dt = durationMin(
|
||||||
numGivingLiveness++;
|
durationFromSpec({
|
||||||
}
|
seconds: 5,
|
||||||
}
|
}),
|
||||||
let dt: Duration;
|
getDurationRemaining(minDue),
|
||||||
if (
|
);
|
||||||
allPending.pendingOperations.length === 0 ||
|
logger.trace(`waiting for at most ${dt.d_ms} ms`)
|
||||||
allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER
|
|
||||||
) {
|
|
||||||
// Wait for 5 seconds
|
|
||||||
dt = { d_ms: 5000 };
|
|
||||||
} else {
|
|
||||||
dt = durationMin({ d_ms: 5000 }, allPending.nextRetryDelay);
|
|
||||||
}
|
|
||||||
const timeout = this.timerGroup.resolveAfter(dt);
|
const timeout = this.timerGroup.resolveAfter(dt);
|
||||||
this.ws.notify({
|
this.ws.notify({
|
||||||
type: NotificationType.WaitingForRetry,
|
type: NotificationType.WaitingForRetry,
|
||||||
numGivingLiveness,
|
numGivingLiveness,
|
||||||
numPending,
|
numPending: pending.pendingOperations.length,
|
||||||
});
|
});
|
||||||
|
// Wait until either the timeout, or we are notified (via the latch)
|
||||||
|
// that more work might be available.
|
||||||
await Promise.race([timeout, this.latch.wait()]);
|
await Promise.race([timeout, this.latch.wait()]);
|
||||||
} else {
|
} else {
|
||||||
// FIXME: maybe be a bit smarter about executing these
|
|
||||||
// operations in parallel?
|
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`running ${pending.pendingOperations.length} pending operations`,
|
`running ${pending.pendingOperations.length} pending operations`,
|
||||||
);
|
);
|
||||||
for (const p of pending.pendingOperations) {
|
for (const p of pending.pendingOperations) {
|
||||||
|
if (!isTimestampExpired(p.timestampDue)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await this.processOnePendingOperation(p);
|
await this.processOnePendingOperation(p);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -650,12 +648,8 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPendingOperations({
|
async getPendingOperations(): Promise<PendingOperationsResponse> {
|
||||||
onlyDue = false,
|
return this.ws.memoGetPending.memo(() => getPendingOperations(this.ws));
|
||||||
} = {}): Promise<PendingOperationsResponse> {
|
|
||||||
return this.ws.memoGetPending.memo(() =>
|
|
||||||
getPendingOperations(this.ws, { onlyDue }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async acceptExchangeTermsOfService(
|
async acceptExchangeTermsOfService(
|
||||||
|
Loading…
Reference in New Issue
Block a user