wallet: make retries more robust and consistent
This commit is contained in:
parent
be489b6b3e
commit
c265e7d019
@ -205,4 +205,12 @@ export class Logger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reportBreak(): void {
|
||||||
|
if (!this.shouldLogError()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const location = new Error("programming error");
|
||||||
|
this.error(`assertion failed: ${location.stack}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
||||||
Headers,
|
Headers,
|
||||||
HttpRequestLibrary,
|
HttpRequestLibrary,
|
||||||
HttpRequestOptions,
|
HttpRequestOptions,
|
||||||
@ -65,13 +66,16 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
`request to origin ${parsedUrl.origin} was throttled`,
|
`request to origin ${parsedUrl.origin} was throttled`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let timeout: number | undefined;
|
let timeoutMs: number | undefined;
|
||||||
if (typeof opt?.timeout?.d_ms === "number") {
|
if (typeof opt?.timeout?.d_ms === "number") {
|
||||||
timeout = opt.timeout.d_ms;
|
timeoutMs = opt.timeout.d_ms;
|
||||||
|
} else {
|
||||||
|
timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
|
||||||
}
|
}
|
||||||
|
// FIXME: Use AbortController / etc. to handle cancellation
|
||||||
let resp: AxiosResponse;
|
let resp: AxiosResponse;
|
||||||
try {
|
try {
|
||||||
resp = await Axios({
|
let respPromise = Axios({
|
||||||
method,
|
method,
|
||||||
url: url,
|
url: url,
|
||||||
responseType: "arraybuffer",
|
responseType: "arraybuffer",
|
||||||
@ -79,9 +83,13 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
validateStatus: () => true,
|
validateStatus: () => true,
|
||||||
transformResponse: (x) => x,
|
transformResponse: (x) => x,
|
||||||
data: body,
|
data: body,
|
||||||
timeout,
|
timeout: timeoutMs,
|
||||||
maxRedirects: 0,
|
maxRedirects: 0,
|
||||||
});
|
});
|
||||||
|
if (opt?.cancellationToken) {
|
||||||
|
respPromise = opt.cancellationToken.racePromise(respPromise);
|
||||||
|
}
|
||||||
|
resp = await respPromise;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw TalerError.fromDetail(
|
throw TalerError.fromDetail(
|
||||||
TalerErrorCode.WALLET_NETWORK_ERROR,
|
TalerErrorCode.WALLET_NETWORK_ERROR,
|
||||||
@ -94,11 +102,13 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const makeText = async (): Promise<string> => {
|
const makeText = async (): Promise<string> => {
|
||||||
|
opt?.cancellationToken?.throwIfCancelled();
|
||||||
const respText = new Uint8Array(resp.data);
|
const respText = new Uint8Array(resp.data);
|
||||||
return bytesToString(respText);
|
return bytesToString(respText);
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeJson = async (): Promise<any> => {
|
const makeJson = async (): Promise<any> => {
|
||||||
|
opt?.cancellationToken?.throwIfCancelled();
|
||||||
let responseJson;
|
let responseJson;
|
||||||
const respText = await makeText();
|
const respText = await makeText();
|
||||||
try {
|
try {
|
||||||
@ -130,6 +140,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
return responseJson;
|
return responseJson;
|
||||||
};
|
};
|
||||||
const makeBytes = async () => {
|
const makeBytes = async () => {
|
||||||
|
opt?.cancellationToken?.throwIfCancelled();
|
||||||
if (typeof resp.data.byteLength !== "number") {
|
if (typeof resp.data.byteLength !== "number") {
|
||||||
throw Error("expected array buffer");
|
throw Error("expected array buffer");
|
||||||
}
|
}
|
||||||
@ -150,6 +161,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
bytes: makeBytes,
|
bytes: makeBytes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
return this.fetch(url, {
|
return this.fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -77,7 +77,9 @@ export interface ReserveOperations {
|
|||||||
processReserve(
|
processReserve(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
forceNow?: boolean,
|
options?: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
},
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,8 +103,10 @@ export interface ExchangeOperations {
|
|||||||
updateExchangeFromUrl(
|
updateExchangeFromUrl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
acceptedFormat?: string[],
|
options?: {
|
||||||
forceNow?: boolean,
|
forceNow?: boolean;
|
||||||
|
cancellationToken?: CancellationToken;
|
||||||
|
},
|
||||||
): Promise<{
|
): Promise<{
|
||||||
exchange: ExchangeRecord;
|
exchange: ExchangeRecord;
|
||||||
exchangeDetails: ExchangeDetailsRecord;
|
exchangeDetails: ExchangeDetailsRecord;
|
||||||
@ -123,7 +127,9 @@ export interface RecoupOperations {
|
|||||||
processRecoupGroup(
|
processRecoupGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
recoupGroupId: string,
|
recoupGroupId: string,
|
||||||
forceNow?: boolean,
|
options?: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
},
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,13 +207,8 @@ export interface InternalWalletState {
|
|||||||
memoGetBalance: AsyncOpMemoSingle<BalancesResponse>;
|
memoGetBalance: AsyncOpMemoSingle<BalancesResponse>;
|
||||||
memoProcessRefresh: AsyncOpMemoMap<void>;
|
memoProcessRefresh: AsyncOpMemoMap<void>;
|
||||||
memoProcessRecoup: AsyncOpMemoMap<void>;
|
memoProcessRecoup: AsyncOpMemoMap<void>;
|
||||||
cryptoApi: TalerCryptoInterface;
|
|
||||||
|
|
||||||
/**
|
cryptoApi: TalerCryptoInterface;
|
||||||
* Cancellation token for the currently running
|
|
||||||
* deposit operation, if any.
|
|
||||||
*/
|
|
||||||
taskCancellationSourceForDeposit?: CancellationToken.Source;
|
|
||||||
|
|
||||||
timerGroup: TimerGroup;
|
timerGroup: TimerGroup;
|
||||||
stopped: boolean;
|
stopped: boolean;
|
||||||
|
@ -17,7 +17,7 @@ Generally, the code to process a pending operation should first increment the
|
|||||||
retryInfo (and reset the lastError) and then process the operation. This way,
|
retryInfo (and reset the lastError) and then process the operation. This way,
|
||||||
it is impossble to forget incrementing the retryInfo.
|
it is impossble to forget incrementing the retryInfo.
|
||||||
|
|
||||||
For each retriable operation, there are usually `reset<Op>Retry`, `increment<Op>Retry` and
|
For each retriable operation, there are usually `setup<Op>Retry`, `increment<Op>Retry` and
|
||||||
`report<Op>Error` operations.
|
`report<Op>Error` operations.
|
||||||
|
|
||||||
Note that this means that _during_ some operation, lastError will be cleared. The UI
|
Note that this means that _during_ some operation, lastError will be cleared. The UI
|
||||||
|
@ -57,7 +57,7 @@ import {
|
|||||||
checkLogicInvariant,
|
checkLogicInvariant,
|
||||||
} from "../../util/invariants.js";
|
} from "../../util/invariants.js";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { initRetryInfo } from "../../util/retries.js";
|
import { resetRetryInfo } from "../../util/retries.js";
|
||||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||||
import { provideBackupState } from "./state.js";
|
import { provideBackupState } from "./state.js";
|
||||||
import { makeEventId, TombstoneTag } from "../transactions.js";
|
import { makeEventId, TombstoneTag } from "../transactions.js";
|
||||||
@ -276,7 +276,7 @@ export async function importBackup(
|
|||||||
protocolVersionRange: backupExchange.protocol_version_range,
|
protocolVersionRange: backupExchange.protocol_version_range,
|
||||||
},
|
},
|
||||||
permanent: true,
|
permanent: true,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
lastUpdate: undefined,
|
lastUpdate: undefined,
|
||||||
nextUpdate: TalerProtocolTimestamp.now(),
|
nextUpdate: TalerProtocolTimestamp.now(),
|
||||||
nextRefreshCheck: TalerProtocolTimestamp.now(),
|
nextRefreshCheck: TalerProtocolTimestamp.now(),
|
||||||
@ -464,7 +464,7 @@ export async function importBackup(
|
|||||||
timestampReserveInfoPosted:
|
timestampReserveInfoPosted:
|
||||||
backupReserve.bank_info?.timestamp_reserve_info_posted,
|
backupReserve.bank_info?.timestamp_reserve_info_posted,
|
||||||
senderWire: backupReserve.sender_wire,
|
senderWire: backupReserve.sender_wire,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
initialWithdrawalGroupId:
|
initialWithdrawalGroupId:
|
||||||
backupReserve.initial_withdrawal_group_id,
|
backupReserve.initial_withdrawal_group_id,
|
||||||
@ -505,7 +505,7 @@ export async function importBackup(
|
|||||||
backupWg.raw_withdrawal_amount,
|
backupWg.raw_withdrawal_amount,
|
||||||
),
|
),
|
||||||
reservePub,
|
reservePub,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
secretSeed: backupWg.secret_seed,
|
secretSeed: backupWg.secret_seed,
|
||||||
timestampStart: backupWg.timestamp_created,
|
timestampStart: backupWg.timestamp_created,
|
||||||
timestampFinish: backupWg.timestamp_finish,
|
timestampFinish: backupWg.timestamp_finish,
|
||||||
@ -618,7 +618,7 @@ export async function importBackup(
|
|||||||
cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv],
|
cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv],
|
||||||
proposalId: backupProposal.proposal_id,
|
proposalId: backupProposal.proposal_id,
|
||||||
repurchaseProposalId: backupProposal.repurchase_proposal_id,
|
repurchaseProposalId: backupProposal.repurchase_proposal_id,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
download,
|
download,
|
||||||
proposalStatus,
|
proposalStatus,
|
||||||
});
|
});
|
||||||
@ -753,7 +753,7 @@ export async function importBackup(
|
|||||||
cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
|
cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
|
||||||
lastPayError: undefined,
|
lastPayError: undefined,
|
||||||
autoRefundDeadline: TalerProtocolTimestamp.never(),
|
autoRefundDeadline: TalerProtocolTimestamp.never(),
|
||||||
refundStatusRetryInfo: initRetryInfo(),
|
refundStatusRetryInfo: resetRetryInfo(),
|
||||||
lastRefundStatusError: undefined,
|
lastRefundStatusError: undefined,
|
||||||
timestampAccept: backupPurchase.timestamp_accept,
|
timestampAccept: backupPurchase.timestamp_accept,
|
||||||
timestampFirstSuccessfulPay:
|
timestampFirstSuccessfulPay:
|
||||||
@ -763,7 +763,7 @@ export async function importBackup(
|
|||||||
lastSessionId: undefined,
|
lastSessionId: undefined,
|
||||||
abortStatus,
|
abortStatus,
|
||||||
// FIXME!
|
// FIXME!
|
||||||
payRetryInfo: initRetryInfo(),
|
payRetryInfo: resetRetryInfo(),
|
||||||
download,
|
download,
|
||||||
paymentSubmitPending:
|
paymentSubmitPending:
|
||||||
!backupPurchase.timestamp_first_successful_pay,
|
!backupPurchase.timestamp_first_successful_pay,
|
||||||
@ -864,7 +864,7 @@ export async function importBackup(
|
|||||||
Amounts.parseOrThrow(x.estimated_output_amount),
|
Amounts.parseOrThrow(x.estimated_output_amount),
|
||||||
),
|
),
|
||||||
refreshSessionPerCoin,
|
refreshSessionPerCoin,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -890,7 +890,7 @@ export async function importBackup(
|
|||||||
merchantBaseUrl: backupTip.exchange_base_url,
|
merchantBaseUrl: backupTip.exchange_base_url,
|
||||||
merchantTipId: backupTip.merchant_tip_id,
|
merchantTipId: backupTip.merchant_tip_id,
|
||||||
pickedUpTimestamp: backupTip.timestamp_finished,
|
pickedUpTimestamp: backupTip.timestamp_finished,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
secretSeed: backupTip.secret_seed,
|
secretSeed: backupTip.secret_seed,
|
||||||
tipAmountEffective: denomsSel.totalCoinValue,
|
tipAmountEffective: denomsSel.totalCoinValue,
|
||||||
tipAmountRaw: Amounts.parseOrThrow(backupTip.tip_amount_raw),
|
tipAmountRaw: Amounts.parseOrThrow(backupTip.tip_amount_raw),
|
||||||
|
@ -89,7 +89,7 @@ import {
|
|||||||
checkLogicInvariant,
|
checkLogicInvariant,
|
||||||
} from "../../util/invariants.js";
|
} from "../../util/invariants.js";
|
||||||
import { GetReadWriteAccess } from "../../util/query.js";
|
import { GetReadWriteAccess } from "../../util/query.js";
|
||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../../util/retries.js";
|
import { resetRetryInfo, updateRetryInfoTimeout } from "../../util/retries.js";
|
||||||
import {
|
import {
|
||||||
checkPaymentByProposalId,
|
checkPaymentByProposalId,
|
||||||
confirmPay,
|
confirmPay,
|
||||||
@ -434,7 +434,7 @@ async function runBackupCycleForProvider(
|
|||||||
// FIXME: Allocate error code for this situation?
|
// FIXME: Allocate error code for this situation?
|
||||||
prov.state = {
|
prov.state = {
|
||||||
tag: BackupProviderStateTag.Retrying,
|
tag: BackupProviderStateTag.Retrying,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
};
|
};
|
||||||
await tx.backupProvider.put(prov);
|
await tx.backupProvider.put(prov);
|
||||||
});
|
});
|
||||||
@ -478,7 +478,7 @@ async function incrementBackupRetryInTx(
|
|||||||
} else if (pr.state.tag === BackupProviderStateTag.Ready) {
|
} else if (pr.state.tag === BackupProviderStateTag.Ready) {
|
||||||
pr.state = {
|
pr.state = {
|
||||||
tag: BackupProviderStateTag.Retrying,
|
tag: BackupProviderStateTag.Retrying,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
lastError: err,
|
lastError: err,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ export async function guardOperationException<T>(
|
|||||||
throw TalerError.fromDetail(
|
throw TalerError.fromDetail(
|
||||||
TalerErrorCode.WALLET_PENDING_OPERATION_FAILED,
|
TalerErrorCode.WALLET_PENDING_OPERATION_FAILED,
|
||||||
{
|
{
|
||||||
innerError: e.errorDetail,
|
innerError: opErr,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -41,11 +41,11 @@ import {
|
|||||||
TrackDepositGroupResponse,
|
TrackDepositGroupResponse,
|
||||||
URL,
|
URL,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { DepositGroupRecord, OperationStatus } from "../db.js";
|
import { DepositGroupRecord, OperationStatus, WireFee } from "../db.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { PayCoinSelection, selectPayCoins } from "../util/coinSelection.js";
|
import { PayCoinSelection, selectPayCoins } from "../util/coinSelection.js";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
import { initRetryInfo, RetryInfo } from "../util/retries.js";
|
import { resetRetryInfo, RetryInfo } from "../util/retries.js";
|
||||||
import { guardOperationException } from "./common.js";
|
import { guardOperationException } from "./common.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import {
|
import {
|
||||||
@ -63,9 +63,15 @@ import { getTotalRefreshCost } from "./refresh.js";
|
|||||||
*/
|
*/
|
||||||
const logger = new Logger("deposits.ts");
|
const logger = new Logger("deposits.ts");
|
||||||
|
|
||||||
async function resetDepositGroupRetry(
|
/**
|
||||||
|
* Set up the retry timeout for a deposit group.
|
||||||
|
*/
|
||||||
|
async function setupDepositGroupRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
depositGroupId: string,
|
depositGroupId: string,
|
||||||
|
options: {
|
||||||
|
resetRetry: boolean;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
@ -76,29 +82,19 @@ async function resetDepositGroupRetry(
|
|||||||
if (!x) {
|
if (!x) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
x.retryInfo = initRetryInfo();
|
if (options.resetRetry) {
|
||||||
|
x.retryInfo = resetRetryInfo();
|
||||||
|
} else {
|
||||||
|
x.retryInfo = RetryInfo.increment(x.retryInfo);
|
||||||
|
}
|
||||||
delete x.lastError;
|
delete x.lastError;
|
||||||
await tx.depositGroups.put(x);
|
await tx.depositGroups.put(x);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function incrementDepositGroupRetry(
|
/**
|
||||||
ws: InternalWalletState,
|
* Report an error that occurred while processing the deposit group.
|
||||||
depositGroupId: string,
|
*/
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({ depositGroups: x.depositGroups }))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const r = await tx.depositGroups.get(depositGroupId);
|
|
||||||
if (!r) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
r.retryInfo = RetryInfo.increment(r.retryInfo);
|
|
||||||
delete r.lastError;
|
|
||||||
await tx.depositGroups.put(r);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reportDepositGroupError(
|
async function reportDepositGroupError(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
depositGroupId: string,
|
depositGroupId: string,
|
||||||
@ -131,9 +127,6 @@ export async function processDepositGroup(
|
|||||||
cancellationToken?: CancellationToken;
|
cancellationToken?: CancellationToken;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (ws.taskCancellationSourceForDeposit) {
|
|
||||||
ws.taskCancellationSourceForDeposit.cancel();
|
|
||||||
}
|
|
||||||
const onOpErr = (err: TalerErrorDetail): Promise<void> =>
|
const onOpErr = (err: TalerErrorDetail): Promise<void> =>
|
||||||
reportDepositGroupError(ws, depositGroupId, err);
|
reportDepositGroupError(ws, depositGroupId, err);
|
||||||
return await guardOperationException(
|
return await guardOperationException(
|
||||||
@ -170,11 +163,7 @@ async function processDepositGroupImpl(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceNow) {
|
await setupDepositGroupRetry(ws, depositGroupId, { resetRetry: forceNow });
|
||||||
await resetDepositGroupRetry(ws, depositGroupId);
|
|
||||||
} else {
|
|
||||||
await incrementDepositGroupRetry(ws, depositGroupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contractData = extractContractData(
|
const contractData = extractContractData(
|
||||||
depositGroup.contractTermsRaw,
|
depositGroup.contractTermsRaw,
|
||||||
@ -315,7 +304,7 @@ export async function trackDepositGroup(
|
|||||||
export async function getFeeForDeposit(
|
export async function getFeeForDeposit(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: GetFeeForDepositRequest,
|
req: GetFeeForDepositRequest,
|
||||||
): Promise<DepositFee> {
|
): Promise<DepositGroupFees> {
|
||||||
const p = parsePaytoUri(req.depositPaytoUri);
|
const p = parsePaytoUri(req.depositPaytoUri);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
throw Error("invalid payto URI");
|
throw Error("invalid payto URI");
|
||||||
@ -370,7 +359,7 @@ export async function getFeeForDeposit(
|
|||||||
throw Error("insufficient funds");
|
throw Error("insufficient funds");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getTotalFeeForDepositAmount(
|
return await getTotalFeesForDepositAmount(
|
||||||
ws,
|
ws,
|
||||||
p.targetType,
|
p.targetType,
|
||||||
amount,
|
amount,
|
||||||
@ -429,14 +418,12 @@ export async function createDepositGroup(
|
|||||||
nonce: noncePair.pub,
|
nonce: noncePair.pub,
|
||||||
wire_transfer_deadline: nowRounded,
|
wire_transfer_deadline: nowRounded,
|
||||||
order_id: "",
|
order_id: "",
|
||||||
// This is always the v2 wire hash, as we're the "merchant" and support v2.
|
|
||||||
h_wire: wireHash,
|
h_wire: wireHash,
|
||||||
// Required for older exchanges.
|
|
||||||
pay_deadline: AbsoluteTime.toTimestamp(
|
pay_deadline: AbsoluteTime.toTimestamp(
|
||||||
AbsoluteTime.addDuration(now, durationFromSpec({ hours: 1 })),
|
AbsoluteTime.addDuration(now, durationFromSpec({ hours: 1 })),
|
||||||
),
|
),
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "",
|
name: "(wallet)",
|
||||||
},
|
},
|
||||||
merchant_pub: merchantPair.pub,
|
merchant_pub: merchantPair.pub,
|
||||||
refund_deadline: TalerProtocolTimestamp.zero(),
|
refund_deadline: TalerProtocolTimestamp.zero(),
|
||||||
@ -505,7 +492,7 @@ export async function createDepositGroup(
|
|||||||
payto_uri: req.depositPaytoUri,
|
payto_uri: req.depositPaytoUri,
|
||||||
salt: wireSalt,
|
salt: wireSalt,
|
||||||
},
|
},
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
operationStatus: OperationStatus.Pending,
|
operationStatus: OperationStatus.Pending,
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
@ -594,8 +581,7 @@ export async function getEffectiveDepositAmount(
|
|||||||
return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
|
return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: rename to DepositGroupFee
|
export interface DepositGroupFees {
|
||||||
export interface DepositFee {
|
|
||||||
coin: AmountJson;
|
coin: AmountJson;
|
||||||
wire: AmountJson;
|
wire: AmountJson;
|
||||||
refresh: AmountJson;
|
refresh: AmountJson;
|
||||||
@ -605,12 +591,12 @@ export interface DepositFee {
|
|||||||
* Get the fee amount that will be charged when trying to deposit the
|
* Get the fee amount that will be charged when trying to deposit the
|
||||||
* specified amount using the selected coins and the wire method.
|
* specified amount using the selected coins and the wire method.
|
||||||
*/
|
*/
|
||||||
export async function getTotalFeeForDepositAmount(
|
export async function getTotalFeesForDepositAmount(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
wireType: string,
|
wireType: string,
|
||||||
total: AmountJson,
|
total: AmountJson,
|
||||||
pcs: PayCoinSelection,
|
pcs: PayCoinSelection,
|
||||||
): Promise<DepositFee> {
|
): Promise<DepositGroupFees> {
|
||||||
const wireFee: AmountJson[] = [];
|
const wireFee: AmountJson[] = [];
|
||||||
const coinFee: AmountJson[] = [];
|
const coinFee: AmountJson[] = [];
|
||||||
const refreshFee: AmountJson[] = [];
|
const refreshFee: AmountJson[] = [];
|
||||||
@ -638,8 +624,6 @@ export async function getTotalFeeForDepositAmount(
|
|||||||
if (!denom) {
|
if (!denom) {
|
||||||
throw Error("can't find denomination to calculate deposit amount");
|
throw Error("can't find denomination to calculate deposit amount");
|
||||||
}
|
}
|
||||||
// const cc = pcs.coinContributions[i]
|
|
||||||
// acc = Amounts.add(acc, cc).amount
|
|
||||||
coinFee.push(denom.feeDeposit);
|
coinFee.push(denom.feeDeposit);
|
||||||
exchangeSet.add(coin.exchangeBaseUrl);
|
exchangeSet.add(coin.exchangeBaseUrl);
|
||||||
|
|
||||||
@ -661,16 +645,15 @@ export async function getTotalFeeForDepositAmount(
|
|||||||
if (!exchangeDetails) {
|
if (!exchangeDetails) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// FIXME/NOTE: the line below _likely_ throws exception
|
const fee = exchangeDetails.wireInfo.feesForType[wireType]?.find(
|
||||||
// about "find method not found on undefined" when the wireType
|
(x) => {
|
||||||
// is not supported by the Exchange.
|
return AbsoluteTime.isBetween(
|
||||||
const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
|
AbsoluteTime.now(),
|
||||||
return AbsoluteTime.isBetween(
|
AbsoluteTime.fromTimestamp(x.startStamp),
|
||||||
AbsoluteTime.now(),
|
AbsoluteTime.fromTimestamp(x.endStamp),
|
||||||
AbsoluteTime.fromTimestamp(x.startStamp),
|
);
|
||||||
AbsoluteTime.fromTimestamp(x.endStamp),
|
},
|
||||||
);
|
)?.wireFee;
|
||||||
})?.wireFee;
|
|
||||||
if (fee) {
|
if (fee) {
|
||||||
wireFee.push(fee);
|
wireFee.push(fee);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
Amounts,
|
Amounts,
|
||||||
|
CancellationToken,
|
||||||
canonicalizeBaseUrl,
|
canonicalizeBaseUrl,
|
||||||
codecForExchangeKeysJson,
|
codecForExchangeKeysJson,
|
||||||
codecForExchangeWireJson,
|
codecForExchangeWireJson,
|
||||||
@ -61,11 +62,7 @@ import {
|
|||||||
readSuccessResponseTextOrThrow,
|
readSuccessResponseTextOrThrow,
|
||||||
} from "../util/http.js";
|
} from "../util/http.js";
|
||||||
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
|
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
|
||||||
import {
|
import { resetRetryInfo, RetryInfo } from "../util/retries.js";
|
||||||
initRetryInfo,
|
|
||||||
RetryInfo,
|
|
||||||
updateRetryInfoTimeout,
|
|
||||||
} from "../util/retries.js";
|
|
||||||
import {
|
import {
|
||||||
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
@ -124,9 +121,12 @@ async function reportExchangeUpdateError(
|
|||||||
ws.notify({ type: NotificationType.ExchangeOperationError, error: err });
|
ws.notify({ type: NotificationType.ExchangeOperationError, error: err });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetExchangeUpdateRetry(
|
async function setupExchangeUpdateRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
|
options: {
|
||||||
|
reset: boolean;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({ exchanges: x.exchanges }))
|
.mktx((x) => ({ exchanges: x.exchanges }))
|
||||||
@ -135,25 +135,12 @@ async function resetExchangeUpdateRetry(
|
|||||||
if (!exchange) {
|
if (!exchange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
delete exchange.lastError;
|
if (options.reset) {
|
||||||
exchange.retryInfo = initRetryInfo();
|
exchange.retryInfo = resetRetryInfo();
|
||||||
await tx.exchanges.put(exchange);
|
} else {
|
||||||
});
|
exchange.retryInfo = RetryInfo.increment(exchange.retryInfo);
|
||||||
}
|
|
||||||
|
|
||||||
async function incrementExchangeUpdateRetry(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
baseUrl: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({ exchanges: x.exchanges }))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const exchange = await tx.exchanges.get(baseUrl);
|
|
||||||
if (!exchange) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
delete exchange.lastError;
|
delete exchange.lastError;
|
||||||
exchange.retryInfo = RetryInfo.increment(exchange.retryInfo);
|
|
||||||
await tx.exchanges.put(exchange);
|
await tx.exchanges.put(exchange);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -376,8 +363,10 @@ async function downloadExchangeWireInfo(
|
|||||||
export async function updateExchangeFromUrl(
|
export async function updateExchangeFromUrl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
acceptedFormat?: string[],
|
options: {
|
||||||
forceNow = false,
|
forceNow?: boolean;
|
||||||
|
cancellationToken?: CancellationToken;
|
||||||
|
} = {},
|
||||||
): Promise<{
|
): Promise<{
|
||||||
exchange: ExchangeRecord;
|
exchange: ExchangeRecord;
|
||||||
exchangeDetails: ExchangeDetailsRecord;
|
exchangeDetails: ExchangeDetailsRecord;
|
||||||
@ -385,7 +374,7 @@ export async function updateExchangeFromUrl(
|
|||||||
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
||||||
reportExchangeUpdateError(ws, baseUrl, e);
|
reportExchangeUpdateError(ws, baseUrl, e);
|
||||||
return await guardOperationException(
|
return await guardOperationException(
|
||||||
() => updateExchangeFromUrlImpl(ws, baseUrl, acceptedFormat, forceNow),
|
() => updateExchangeFromUrlImpl(ws, baseUrl, options),
|
||||||
onOpErr,
|
onOpErr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -409,7 +398,7 @@ async function provideExchangeRecord(
|
|||||||
const r: ExchangeRecord = {
|
const r: ExchangeRecord = {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
detailsPointer: undefined,
|
detailsPointer: undefined,
|
||||||
lastUpdate: undefined,
|
lastUpdate: undefined,
|
||||||
nextUpdate: AbsoluteTime.toTimestamp(now),
|
nextUpdate: AbsoluteTime.toTimestamp(now),
|
||||||
@ -552,12 +541,15 @@ export async function downloadTosFromAcceptedFormat(
|
|||||||
async function updateExchangeFromUrlImpl(
|
async function updateExchangeFromUrlImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
acceptedFormat?: string[],
|
options: {
|
||||||
forceNow = false,
|
forceNow?: boolean;
|
||||||
|
cancellationToken?: CancellationToken;
|
||||||
|
} = {},
|
||||||
): Promise<{
|
): Promise<{
|
||||||
exchange: ExchangeRecord;
|
exchange: ExchangeRecord;
|
||||||
exchangeDetails: ExchangeDetailsRecord;
|
exchangeDetails: ExchangeDetailsRecord;
|
||||||
}> {
|
}> {
|
||||||
|
const forceNow = options.forceNow ?? false;
|
||||||
logger.info(`updating exchange info for ${baseUrl}, forced: ${forceNow}`);
|
logger.info(`updating exchange info for ${baseUrl}, forced: ${forceNow}`);
|
||||||
const now = AbsoluteTime.now();
|
const now = AbsoluteTime.now();
|
||||||
baseUrl = canonicalizeBaseUrl(baseUrl);
|
baseUrl = canonicalizeBaseUrl(baseUrl);
|
||||||
@ -577,11 +569,7 @@ async function updateExchangeFromUrlImpl(
|
|||||||
return { exchange, exchangeDetails };
|
return { exchange, exchangeDetails };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceNow) {
|
await setupExchangeUpdateRetry(ws, baseUrl, { reset: forceNow });
|
||||||
await resetExchangeUpdateRetry(ws, baseUrl);
|
|
||||||
} else {
|
|
||||||
await incrementExchangeUpdateRetry(ws, baseUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("updating exchange /keys info");
|
logger.info("updating exchange /keys info");
|
||||||
|
|
||||||
@ -617,7 +605,7 @@ async function updateExchangeFromUrlImpl(
|
|||||||
ws,
|
ws,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
timeout,
|
timeout,
|
||||||
acceptedFormat,
|
["text/plain"],
|
||||||
);
|
);
|
||||||
const tosHasBeenAccepted =
|
const tosHasBeenAccepted =
|
||||||
exchangeDetails?.termsOfServiceAcceptedEtag === tosDownload.tosEtag;
|
exchangeDetails?.termsOfServiceAcceptedEtag === tosDownload.tosEtag;
|
||||||
|
@ -97,7 +97,7 @@ import {
|
|||||||
import { GetReadWriteAccess } from "../util/query.js";
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
import {
|
import {
|
||||||
getRetryDuration,
|
getRetryDuration,
|
||||||
initRetryInfo,
|
resetRetryInfo,
|
||||||
RetryInfo,
|
RetryInfo,
|
||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
} from "../util/retries.js";
|
} from "../util/retries.js";
|
||||||
@ -428,8 +428,8 @@ async function recordConfirmPay(
|
|||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
lastPayError: undefined,
|
lastPayError: undefined,
|
||||||
lastRefundStatusError: undefined,
|
lastRefundStatusError: undefined,
|
||||||
payRetryInfo: initRetryInfo(),
|
payRetryInfo: resetRetryInfo(),
|
||||||
refundStatusRetryInfo: initRetryInfo(),
|
refundStatusRetryInfo: resetRetryInfo(),
|
||||||
refundQueryRequested: false,
|
refundQueryRequested: false,
|
||||||
timestampFirstSuccessfulPay: undefined,
|
timestampFirstSuccessfulPay: undefined,
|
||||||
autoRefundDeadline: undefined,
|
autoRefundDeadline: undefined,
|
||||||
@ -453,7 +453,7 @@ async function recordConfirmPay(
|
|||||||
if (p) {
|
if (p) {
|
||||||
p.proposalStatus = ProposalStatus.Accepted;
|
p.proposalStatus = ProposalStatus.Accepted;
|
||||||
delete p.lastError;
|
delete p.lastError;
|
||||||
p.retryInfo = initRetryInfo();
|
delete p.retryInfo;
|
||||||
await tx.proposals.put(p);
|
await tx.proposals.put(p);
|
||||||
}
|
}
|
||||||
await tx.purchases.put(t);
|
await tx.purchases.put(t);
|
||||||
@ -491,9 +491,12 @@ async function reportProposalError(
|
|||||||
ws.notify({ type: NotificationType.ProposalOperationError, error: err });
|
ws.notify({ type: NotificationType.ProposalOperationError, error: err });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function incrementProposalRetry(
|
async function setupProposalRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
|
options: {
|
||||||
|
reset: boolean;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({ proposals: x.proposals }))
|
.mktx((x) => ({ proposals: x.proposals }))
|
||||||
@ -502,47 +505,37 @@ async function incrementProposalRetry(
|
|||||||
if (!pr) {
|
if (!pr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!pr.retryInfo) {
|
if (options.reset) {
|
||||||
return;
|
pr.retryInfo = resetRetryInfo();
|
||||||
} else {
|
} else {
|
||||||
pr.retryInfo.retryCounter++;
|
pr.retryInfo = RetryInfo.increment(pr.retryInfo);
|
||||||
updateRetryInfoTimeout(pr.retryInfo);
|
|
||||||
}
|
}
|
||||||
delete pr.lastError;
|
delete pr.lastError;
|
||||||
await tx.proposals.put(pr);
|
await tx.proposals.put(pr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetPurchasePayRetry(
|
async function setupPurchasePayRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
|
options: {
|
||||||
|
reset: boolean;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({ purchases: x.purchases }))
|
.mktx((x) => ({ purchases: x.purchases }))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const p = await tx.purchases.get(proposalId);
|
const p = await tx.purchases.get(proposalId);
|
||||||
if (p) {
|
if (!p) {
|
||||||
p.payRetryInfo = initRetryInfo();
|
|
||||||
delete p.lastPayError;
|
|
||||||
await tx.purchases.put(p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function incrementPurchasePayRetry(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
proposalId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({ purchases: x.purchases }))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const pr = await tx.purchases.get(proposalId);
|
|
||||||
if (!pr) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pr.payRetryInfo = RetryInfo.increment(pr.payRetryInfo);
|
if (options.reset) {
|
||||||
delete pr.lastPayError;
|
p.payRetryInfo = resetRetryInfo();
|
||||||
await tx.purchases.put(pr);
|
} else {
|
||||||
|
p.payRetryInfo = RetryInfo.increment(p.payRetryInfo);
|
||||||
|
}
|
||||||
|
delete p.lastPayError;
|
||||||
|
await tx.purchases.put(p);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,32 +565,18 @@ async function reportPurchasePayError(
|
|||||||
export async function processDownloadProposal(
|
export async function processDownloadProposal(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (err: TalerErrorDetail): Promise<void> =>
|
const onOpErr = (err: TalerErrorDetail): Promise<void> =>
|
||||||
reportProposalError(ws, proposalId, err);
|
reportProposalError(ws, proposalId, err);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processDownloadProposalImpl(ws, proposalId, forceNow),
|
() => processDownloadProposalImpl(ws, proposalId, options),
|
||||||
onOpErr,
|
onOpErr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetDownloadProposalRetry(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
proposalId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({ proposals: x.proposals }))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const p = await tx.proposals.get(proposalId);
|
|
||||||
if (p) {
|
|
||||||
p.retryInfo = initRetryInfo();
|
|
||||||
delete p.lastError;
|
|
||||||
await tx.proposals.put(p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function failProposalPermanently(
|
async function failProposalPermanently(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
@ -678,8 +657,11 @@ export function extractContractData(
|
|||||||
async function processDownloadProposalImpl(
|
async function processDownloadProposalImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
forceNow: boolean,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const forceNow = options.forceNow ?? false;
|
||||||
const proposal = await ws.db
|
const proposal = await ws.db
|
||||||
.mktx((x) => ({ proposals: x.proposals }))
|
.mktx((x) => ({ proposals: x.proposals }))
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
@ -694,11 +676,7 @@ async function processDownloadProposalImpl(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceNow) {
|
await setupProposalRetry(ws, proposalId, { reset: forceNow });
|
||||||
await resetDownloadProposalRetry(ws, proposalId);
|
|
||||||
} else {
|
|
||||||
await incrementProposalRetry(ws, proposalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderClaimUrl = new URL(
|
const orderClaimUrl = new URL(
|
||||||
`orders/${proposal.orderId}/claim`,
|
`orders/${proposal.orderId}/claim`,
|
||||||
@ -946,7 +924,7 @@ async function startDownloadProposal(
|
|||||||
proposalId: proposalId,
|
proposalId: proposalId,
|
||||||
proposalStatus: ProposalStatus.Downloading,
|
proposalStatus: ProposalStatus.Downloading,
|
||||||
repurchaseProposalId: undefined,
|
repurchaseProposalId: undefined,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
downloadSessionId: sessionId,
|
downloadSessionId: sessionId,
|
||||||
};
|
};
|
||||||
@ -994,7 +972,7 @@ async function storeFirstPaySuccess(
|
|||||||
purchase.paymentSubmitPending = false;
|
purchase.paymentSubmitPending = false;
|
||||||
purchase.lastPayError = undefined;
|
purchase.lastPayError = undefined;
|
||||||
purchase.lastSessionId = sessionId;
|
purchase.lastSessionId = sessionId;
|
||||||
purchase.payRetryInfo = initRetryInfo();
|
purchase.payRetryInfo = resetRetryInfo();
|
||||||
purchase.merchantPaySig = paySig;
|
purchase.merchantPaySig = paySig;
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
const protoAr = purchase.download.contractData.autoRefund;
|
const protoAr = purchase.download.contractData.autoRefund;
|
||||||
@ -1002,7 +980,7 @@ async function storeFirstPaySuccess(
|
|||||||
const ar = Duration.fromTalerProtocolDuration(protoAr);
|
const ar = Duration.fromTalerProtocolDuration(protoAr);
|
||||||
logger.info("auto_refund present");
|
logger.info("auto_refund present");
|
||||||
purchase.refundQueryRequested = true;
|
purchase.refundQueryRequested = true;
|
||||||
purchase.refundStatusRetryInfo = initRetryInfo();
|
purchase.refundStatusRetryInfo = resetRetryInfo();
|
||||||
purchase.lastRefundStatusError = undefined;
|
purchase.lastRefundStatusError = undefined;
|
||||||
purchase.autoRefundDeadline = AbsoluteTime.toTimestamp(
|
purchase.autoRefundDeadline = AbsoluteTime.toTimestamp(
|
||||||
AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
|
AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
|
||||||
@ -1033,7 +1011,7 @@ async function storePayReplaySuccess(
|
|||||||
}
|
}
|
||||||
purchase.paymentSubmitPending = false;
|
purchase.paymentSubmitPending = false;
|
||||||
purchase.lastPayError = undefined;
|
purchase.lastPayError = undefined;
|
||||||
purchase.payRetryInfo = initRetryInfo();
|
purchase.payRetryInfo = resetRetryInfo();
|
||||||
purchase.lastSessionId = sessionId;
|
purchase.lastSessionId = sessionId;
|
||||||
await tx.purchases.put(purchase);
|
await tx.purchases.put(purchase);
|
||||||
});
|
});
|
||||||
@ -1289,7 +1267,7 @@ export async function checkPaymentByProposalId(
|
|||||||
p.paymentSubmitPending = true;
|
p.paymentSubmitPending = true;
|
||||||
await tx.purchases.put(p);
|
await tx.purchases.put(p);
|
||||||
});
|
});
|
||||||
const r = await processPurchasePay(ws, proposalId, true);
|
const r = await processPurchasePay(ws, proposalId, { forceNow: true });
|
||||||
if (r.type !== ConfirmPayResultType.Done) {
|
if (r.type !== ConfirmPayResultType.Done) {
|
||||||
throw Error("submitting pay failed");
|
throw Error("submitting pay failed");
|
||||||
}
|
}
|
||||||
@ -1466,7 +1444,7 @@ export async function confirmPay(
|
|||||||
|
|
||||||
if (existingPurchase) {
|
if (existingPurchase) {
|
||||||
logger.trace("confirmPay: submitting payment for existing purchase");
|
logger.trace("confirmPay: submitting payment for existing purchase");
|
||||||
return await processPurchasePay(ws, proposalId, true);
|
return await processPurchasePay(ws, proposalId, { forceNow: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace("confirmPay: purchase record does not exist yet");
|
logger.trace("confirmPay: purchase record does not exist yet");
|
||||||
@ -1516,18 +1494,20 @@ export async function confirmPay(
|
|||||||
sessionIdOverride,
|
sessionIdOverride,
|
||||||
);
|
);
|
||||||
|
|
||||||
return await processPurchasePay(ws, proposalId, true);
|
return await processPurchasePay(ws, proposalId, { forceNow: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processPurchasePay(
|
export async function processPurchasePay(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<ConfirmPayResult> {
|
): Promise<ConfirmPayResult> {
|
||||||
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
||||||
reportPurchasePayError(ws, proposalId, e);
|
reportPurchasePayError(ws, proposalId, e);
|
||||||
return await guardOperationException(
|
return await guardOperationException(
|
||||||
() => processPurchasePayImpl(ws, proposalId, forceNow),
|
() => processPurchasePayImpl(ws, proposalId, options),
|
||||||
onOpErr,
|
onOpErr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1535,8 +1515,11 @@ export async function processPurchasePay(
|
|||||||
async function processPurchasePayImpl(
|
async function processPurchasePayImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
forceNow: boolean,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<ConfirmPayResult> {
|
): Promise<ConfirmPayResult> {
|
||||||
|
const forceNow = options.forceNow ?? false;
|
||||||
const purchase = await ws.db
|
const purchase = await ws.db
|
||||||
.mktx((x) => ({ purchases: x.purchases }))
|
.mktx((x) => ({ purchases: x.purchases }))
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
@ -1559,11 +1542,7 @@ async function processPurchasePayImpl(
|
|||||||
lastError: purchase.lastPayError,
|
lastError: purchase.lastPayError,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (forceNow) {
|
await setupPurchasePayRetry(ws, proposalId, { reset: forceNow });
|
||||||
await resetPurchasePayRetry(ws, proposalId);
|
|
||||||
} else {
|
|
||||||
await incrementPurchasePayRetry(ws, proposalId);
|
|
||||||
}
|
|
||||||
logger.trace(`processing purchase pay ${proposalId}`);
|
logger.trace(`processing purchase pay ${proposalId}`);
|
||||||
|
|
||||||
const sessionId = purchase.lastSessionId;
|
const sessionId = purchase.lastSessionId;
|
||||||
|
@ -51,18 +51,17 @@ async function gatherExchangePending(
|
|||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingTaskType.ExchangeUpdate,
|
type: PendingTaskType.ExchangeUpdate,
|
||||||
givesLifeness: false,
|
givesLifeness: false,
|
||||||
timestampDue: e.lastError
|
timestampDue:
|
||||||
? e.retryInfo.nextRetry
|
e.retryInfo?.nextRetry ?? AbsoluteTime.fromTimestamp(e.nextUpdate),
|
||||||
: AbsoluteTime.fromTimestamp(e.nextUpdate),
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
exchangeBaseUrl: e.baseUrl,
|
||||||
lastError: e.lastError,
|
lastError: e.lastError,
|
||||||
});
|
});
|
||||||
|
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingTaskType.ExchangeCheckRefresh,
|
type: PendingTaskType.ExchangeCheckRefresh,
|
||||||
timestampDue: e.lastError
|
timestampDue:
|
||||||
? e.retryInfo.nextRetry
|
e.retryInfo?.nextRetry ??
|
||||||
: AbsoluteTime.fromTimestamp(e.nextRefreshCheck),
|
AbsoluteTime.fromTimestamp(e.nextRefreshCheck),
|
||||||
givesLifeness: false,
|
givesLifeness: false,
|
||||||
exchangeBaseUrl: e.baseUrl,
|
exchangeBaseUrl: e.baseUrl,
|
||||||
});
|
});
|
||||||
|
@ -48,7 +48,11 @@ import {
|
|||||||
|
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
import { Logger, URL } from "@gnu-taler/taler-util";
|
import { Logger, URL } from "@gnu-taler/taler-util";
|
||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
|
import {
|
||||||
|
resetRetryInfo,
|
||||||
|
RetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
|
} from "../util/retries.js";
|
||||||
import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
|
import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
|
||||||
import { getReserveRequestTimeout, processReserve } from "./reserves.js";
|
import { getReserveRequestTimeout, processReserve } from "./reserves.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
@ -57,10 +61,36 @@ import { guardOperationException } from "./common.js";
|
|||||||
|
|
||||||
const logger = new Logger("operations/recoup.ts");
|
const logger = new Logger("operations/recoup.ts");
|
||||||
|
|
||||||
async function incrementRecoupRetry(
|
async function setupRecoupRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
recoupGroupId: string,
|
recoupGroupId: string,
|
||||||
err: TalerErrorDetail | undefined,
|
options: {
|
||||||
|
reset: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
recoupGroups: x.recoupGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const r = await tx.recoupGroups.get(recoupGroupId);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.reset) {
|
||||||
|
r.retryInfo = resetRetryInfo();
|
||||||
|
} else {
|
||||||
|
r.retryInfo = RetryInfo.increment(r.retryInfo);
|
||||||
|
}
|
||||||
|
delete r.lastError;
|
||||||
|
await tx.recoupGroups.put(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reportRecoupError(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
recoupGroupId: string,
|
||||||
|
err: TalerErrorDetail,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
@ -72,16 +102,14 @@ async function incrementRecoupRetry(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!r.retryInfo) {
|
if (!r.retryInfo) {
|
||||||
return;
|
logger.error(
|
||||||
|
"reporting error for inactive recoup group (no retry info)",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
r.retryInfo.retryCounter++;
|
|
||||||
updateRetryInfoTimeout(r.retryInfo);
|
|
||||||
r.lastError = err;
|
r.lastError = err;
|
||||||
await tx.recoupGroups.put(r);
|
await tx.recoupGroups.put(r);
|
||||||
});
|
});
|
||||||
if (err) {
|
ws.notify({ type: NotificationType.RecoupOperationError, error: err });
|
||||||
ws.notify({ type: NotificationType.RecoupOperationError, error: err });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function putGroupAsFinished(
|
async function putGroupAsFinished(
|
||||||
@ -111,7 +139,7 @@ async function putGroupAsFinished(
|
|||||||
if (allFinished) {
|
if (allFinished) {
|
||||||
logger.info("all recoups of recoup group are finished");
|
logger.info("all recoups of recoup group are finished");
|
||||||
recoupGroup.timestampFinished = TalerProtocolTimestamp.now();
|
recoupGroup.timestampFinished = TalerProtocolTimestamp.now();
|
||||||
recoupGroup.retryInfo = initRetryInfo();
|
recoupGroup.retryInfo = resetRetryInfo();
|
||||||
recoupGroup.lastError = undefined;
|
recoupGroup.lastError = undefined;
|
||||||
if (recoupGroup.scheduleRefreshCoins.length > 0) {
|
if (recoupGroup.scheduleRefreshCoins.length > 0) {
|
||||||
const refreshGroupId = await createRefreshGroup(
|
const refreshGroupId = await createRefreshGroup(
|
||||||
@ -250,7 +278,7 @@ async function recoupWithdrawCoin(
|
|||||||
const currency = updatedCoin.currentAmount.currency;
|
const currency = updatedCoin.currentAmount.currency;
|
||||||
updatedCoin.currentAmount = Amounts.getZero(currency);
|
updatedCoin.currentAmount = Amounts.getZero(currency);
|
||||||
updatedReserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
updatedReserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
||||||
updatedReserve.retryInfo = initRetryInfo();
|
updatedReserve.retryInfo = resetRetryInfo();
|
||||||
updatedReserve.operationStatus = OperationStatus.Pending;
|
updatedReserve.operationStatus = OperationStatus.Pending;
|
||||||
await tx.coins.put(updatedCoin);
|
await tx.coins.put(updatedCoin);
|
||||||
await tx.reserves.put(updatedReserve);
|
await tx.reserves.put(updatedReserve);
|
||||||
@ -361,33 +389,18 @@ async function recoupRefreshCoin(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetRecoupGroupRetry(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
recoupGroupId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
recoupGroups: x.recoupGroups,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const x = await tx.recoupGroups.get(recoupGroupId);
|
|
||||||
if (x) {
|
|
||||||
x.retryInfo = initRetryInfo();
|
|
||||||
await tx.recoupGroups.put(x);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processRecoupGroup(
|
export async function processRecoupGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
recoupGroupId: string,
|
recoupGroupId: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
|
await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
|
||||||
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
||||||
incrementRecoupRetry(ws, recoupGroupId, e);
|
reportRecoupError(ws, recoupGroupId, e);
|
||||||
return await guardOperationException(
|
return await guardOperationException(
|
||||||
async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
|
async () => await processRecoupGroupImpl(ws, recoupGroupId, options),
|
||||||
onOpErr,
|
onOpErr,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -396,11 +409,12 @@ export async function processRecoupGroup(
|
|||||||
async function processRecoupGroupImpl(
|
async function processRecoupGroupImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
recoupGroupId: string,
|
recoupGroupId: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (forceNow) {
|
const forceNow = options.forceNow ?? false;
|
||||||
await resetRecoupGroupRetry(ws, recoupGroupId);
|
await setupRecoupRetry(ws, recoupGroupId, { reset: forceNow });
|
||||||
}
|
|
||||||
const recoupGroup = await ws.db
|
const recoupGroup = await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
recoupGroups: x.recoupGroups,
|
recoupGroups: x.recoupGroups,
|
||||||
@ -444,7 +458,7 @@ async function processRecoupGroupImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const r of reserveSet.values()) {
|
for (const r of reserveSet.values()) {
|
||||||
processReserve(ws, r, true).catch((e) => {
|
processReserve(ws, r, { forceNow: true }).catch((e) => {
|
||||||
logger.error(`processing reserve ${r} after recoup failed`);
|
logger.error(`processing reserve ${r} after recoup failed`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -468,7 +482,7 @@ export async function createRecoupGroup(
|
|||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
timestampFinished: undefined,
|
timestampFinished: undefined,
|
||||||
timestampStarted: TalerProtocolTimestamp.now(),
|
timestampStarted: TalerProtocolTimestamp.now(),
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
recoupFinishedPerCoin: coinPubs.map(() => false),
|
recoupFinishedPerCoin: coinPubs.map(() => false),
|
||||||
// Will be populated later
|
// Will be populated later
|
||||||
oldAmountPerCoin: [],
|
oldAmountPerCoin: [],
|
||||||
|
@ -53,7 +53,11 @@ import {
|
|||||||
} from "../util/http.js";
|
} from "../util/http.js";
|
||||||
import { checkDbInvariant } from "../util/invariants.js";
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
|
import {
|
||||||
|
resetRetryInfo,
|
||||||
|
RetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
|
} from "../util/retries.js";
|
||||||
import {
|
import {
|
||||||
Duration,
|
Duration,
|
||||||
durationFromSpec,
|
durationFromSpec,
|
||||||
@ -130,11 +134,11 @@ function updateGroupStatus(rg: RefreshGroupRecord): void {
|
|||||||
if (allDone) {
|
if (allDone) {
|
||||||
if (anyFrozen) {
|
if (anyFrozen) {
|
||||||
rg.frozen = true;
|
rg.frozen = true;
|
||||||
rg.retryInfo = initRetryInfo();
|
rg.retryInfo = resetRetryInfo();
|
||||||
} else {
|
} else {
|
||||||
rg.timestampFinished = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
rg.timestampFinished = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
||||||
rg.operationStatus = OperationStatus.Finished;
|
rg.operationStatus = OperationStatus.Finished;
|
||||||
rg.retryInfo = initRetryInfo();
|
rg.retryInfo = resetRetryInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -712,7 +716,33 @@ async function refreshReveal(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function incrementRefreshRetry(
|
async function setupRefreshRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
refreshGroupId: string,
|
||||||
|
options: {
|
||||||
|
reset: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
refreshGroups: x.refreshGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const r = await tx.refreshGroups.get(refreshGroupId);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.reset) {
|
||||||
|
r.retryInfo = resetRetryInfo();
|
||||||
|
} else {
|
||||||
|
r.retryInfo = RetryInfo.increment(r.retryInfo);
|
||||||
|
}
|
||||||
|
delete r.lastError;
|
||||||
|
await tx.refreshGroups.put(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reportRefreshError(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refreshGroupId: string,
|
refreshGroupId: string,
|
||||||
err: TalerErrorDetail | undefined,
|
err: TalerErrorDetail | undefined,
|
||||||
@ -727,10 +757,10 @@ async function incrementRefreshRetry(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!r.retryInfo) {
|
if (!r.retryInfo) {
|
||||||
return;
|
logger.error(
|
||||||
|
"reported error for inactive refresh group (no retry info)",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
r.retryInfo.retryCounter++;
|
|
||||||
updateRetryInfoTimeout(r.retryInfo);
|
|
||||||
r.lastError = err;
|
r.lastError = err;
|
||||||
await tx.refreshGroups.put(r);
|
await tx.refreshGroups.put(r);
|
||||||
});
|
});
|
||||||
@ -745,44 +775,31 @@ async function incrementRefreshRetry(
|
|||||||
export async function processRefreshGroup(
|
export async function processRefreshGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refreshGroupId: string,
|
refreshGroupId: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
|
await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
|
||||||
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
||||||
incrementRefreshRetry(ws, refreshGroupId, e);
|
reportRefreshError(ws, refreshGroupId, e);
|
||||||
return await guardOperationException(
|
return await guardOperationException(
|
||||||
async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
|
async () => await processRefreshGroupImpl(ws, refreshGroupId, options),
|
||||||
onOpErr,
|
onOpErr,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetRefreshGroupRetry(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
refreshGroupId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
refreshGroups: x.refreshGroups,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const x = await tx.refreshGroups.get(refreshGroupId);
|
|
||||||
if (x) {
|
|
||||||
x.retryInfo = initRetryInfo();
|
|
||||||
await tx.refreshGroups.put(x);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processRefreshGroupImpl(
|
async function processRefreshGroupImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refreshGroupId: string,
|
refreshGroupId: string,
|
||||||
forceNow: boolean,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const forceNow = options.forceNow ?? false;
|
||||||
logger.info(`processing refresh group ${refreshGroupId}`);
|
logger.info(`processing refresh group ${refreshGroupId}`);
|
||||||
if (forceNow) {
|
await setupRefreshRetry(ws, refreshGroupId, { reset: forceNow });
|
||||||
await resetRefreshGroupRetry(ws, refreshGroupId);
|
|
||||||
}
|
|
||||||
const refreshGroup = await ws.db
|
const refreshGroup = await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
refreshGroups: x.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
@ -939,7 +956,7 @@ export async function createRefreshGroup(
|
|||||||
reason,
|
reason,
|
||||||
refreshGroupId,
|
refreshGroupId,
|
||||||
refreshSessionPerCoin: oldCoinPubs.map(() => undefined),
|
refreshSessionPerCoin: oldCoinPubs.map(() => undefined),
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
inputPerCoin,
|
inputPerCoin,
|
||||||
estimatedOutputPerCoin,
|
estimatedOutputPerCoin,
|
||||||
timestampCreated: TalerProtocolTimestamp.now(),
|
timestampCreated: TalerProtocolTimestamp.now(),
|
||||||
@ -994,7 +1011,9 @@ export async function autoRefresh(
|
|||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.info(`doing auto-refresh check for '${exchangeBaseUrl}'`);
|
logger.info(`doing auto-refresh check for '${exchangeBaseUrl}'`);
|
||||||
await updateExchangeFromUrl(ws, exchangeBaseUrl, undefined, true);
|
await updateExchangeFromUrl(ws, exchangeBaseUrl, {
|
||||||
|
forceNow: true,
|
||||||
|
});
|
||||||
let minCheckThreshold = AbsoluteTime.addDuration(
|
let minCheckThreshold = AbsoluteTime.addDuration(
|
||||||
AbsoluteTime.now(),
|
AbsoluteTime.now(),
|
||||||
durationFromSpec({ days: 1 }),
|
durationFromSpec({ days: 1 }),
|
||||||
|
@ -58,37 +58,54 @@ import {
|
|||||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
import { checkDbInvariant } from "../util/invariants.js";
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
import { GetReadWriteAccess } from "../util/query.js";
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
|
import {
|
||||||
|
resetRetryInfo,
|
||||||
|
RetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
|
} from "../util/retries.js";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { guardOperationException } from "./common.js";
|
import { guardOperationException } from "./common.js";
|
||||||
|
|
||||||
const logger = new Logger("refund.ts");
|
const logger = new Logger("refund.ts");
|
||||||
|
|
||||||
async function resetPurchaseQueryRefundRetry(
|
/**
|
||||||
|
* Retry querying and applying refunds for an order later.
|
||||||
|
*/
|
||||||
|
async function setupPurchaseQueryRefundRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
|
options: {
|
||||||
|
reset: boolean;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
purchases: x.purchases,
|
purchases: x.purchases,
|
||||||
}))
|
}))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const x = await tx.purchases.get(proposalId);
|
const pr = await tx.purchases.get(proposalId);
|
||||||
if (x) {
|
if (!pr) {
|
||||||
x.refundStatusRetryInfo = initRetryInfo();
|
return;
|
||||||
await tx.purchases.put(x);
|
|
||||||
}
|
}
|
||||||
|
if (options.reset) {
|
||||||
|
pr.refundStatusRetryInfo = resetRetryInfo();
|
||||||
|
} else {
|
||||||
|
pr.refundStatusRetryInfo = RetryInfo.increment(
|
||||||
|
pr.refundStatusRetryInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await tx.purchases.put(pr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retry querying and applying refunds for an order later.
|
* Report an error that happending when querying for a purchase's refund.
|
||||||
*/
|
*/
|
||||||
async function incrementPurchaseQueryRefundRetry(
|
async function reportPurchaseQueryRefundError(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
err: TalerErrorDetail | undefined,
|
err: TalerErrorDetail,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
@ -100,10 +117,10 @@ async function incrementPurchaseQueryRefundRetry(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!pr.refundStatusRetryInfo) {
|
if (!pr.refundStatusRetryInfo) {
|
||||||
return;
|
logger.error(
|
||||||
|
"reported error on an inactive purchase (no refund status retry info)",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
pr.refundStatusRetryInfo.retryCounter++;
|
|
||||||
updateRetryInfoTimeout(pr.refundStatusRetryInfo);
|
|
||||||
pr.lastRefundStatusError = err;
|
pr.lastRefundStatusError = err;
|
||||||
await tx.purchases.put(pr);
|
await tx.purchases.put(pr);
|
||||||
});
|
});
|
||||||
@ -425,7 +442,7 @@ async function acceptRefunds(
|
|||||||
if (queryDone) {
|
if (queryDone) {
|
||||||
p.timestampLastRefundStatus = now;
|
p.timestampLastRefundStatus = now;
|
||||||
p.lastRefundStatusError = undefined;
|
p.lastRefundStatusError = undefined;
|
||||||
p.refundStatusRetryInfo = initRetryInfo();
|
p.refundStatusRetryInfo = resetRetryInfo();
|
||||||
p.refundQueryRequested = false;
|
p.refundQueryRequested = false;
|
||||||
if (p.abortStatus === AbortStatus.AbortRefund) {
|
if (p.abortStatus === AbortStatus.AbortRefund) {
|
||||||
p.abortStatus = AbortStatus.AbortFinished;
|
p.abortStatus = AbortStatus.AbortFinished;
|
||||||
@ -506,7 +523,7 @@ export async function applyRefund(
|
|||||||
}
|
}
|
||||||
p.refundQueryRequested = true;
|
p.refundQueryRequested = true;
|
||||||
p.lastRefundStatusError = undefined;
|
p.lastRefundStatusError = undefined;
|
||||||
p.refundStatusRetryInfo = initRetryInfo();
|
p.refundStatusRetryInfo = resetRetryInfo();
|
||||||
await tx.purchases.put(p);
|
await tx.purchases.put(p);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@ -515,7 +532,10 @@ export async function applyRefund(
|
|||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.RefundStarted,
|
type: NotificationType.RefundStarted,
|
||||||
});
|
});
|
||||||
await processPurchaseQueryRefundImpl(ws, proposalId, true, false);
|
await processPurchaseQueryRefundImpl(ws, proposalId, {
|
||||||
|
forceNow: true,
|
||||||
|
waitForAutoRefund: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
purchase = await ws.db
|
purchase = await ws.db
|
||||||
@ -590,12 +610,15 @@ export async function applyRefund(
|
|||||||
export async function processPurchaseQueryRefund(
|
export async function processPurchaseQueryRefund(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
waitForAutoRefund?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
||||||
incrementPurchaseQueryRefundRetry(ws, proposalId, e);
|
reportPurchaseQueryRefundError(ws, proposalId, e);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processPurchaseQueryRefundImpl(ws, proposalId, forceNow, true),
|
() => processPurchaseQueryRefundImpl(ws, proposalId, options),
|
||||||
onOpErr,
|
onOpErr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -603,12 +626,14 @@ export async function processPurchaseQueryRefund(
|
|||||||
async function processPurchaseQueryRefundImpl(
|
async function processPurchaseQueryRefundImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
forceNow: boolean,
|
options: {
|
||||||
waitForAutoRefund: boolean,
|
forceNow?: boolean;
|
||||||
|
waitForAutoRefund?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (forceNow) {
|
const forceNow = options.forceNow ?? false;
|
||||||
await resetPurchaseQueryRefundRetry(ws, proposalId);
|
const waitForAutoRefund = options.waitForAutoRefund ?? false;
|
||||||
}
|
await setupPurchaseQueryRefundRetry(ws, proposalId, { reset: forceNow });
|
||||||
const purchase = await ws.db
|
const purchase = await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
purchases: x.purchases,
|
purchases: x.purchases,
|
||||||
@ -650,7 +675,7 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
codecForMerchantOrderStatusPaid(),
|
codecForMerchantOrderStatusPaid(),
|
||||||
);
|
);
|
||||||
if (!orderStatus.refunded) {
|
if (!orderStatus.refunded) {
|
||||||
incrementPurchaseQueryRefundRetry(ws, proposalId, undefined);
|
// Wait for retry ...
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -666,11 +691,6 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
h_contract: purchase.download.contractData.contractTermsHash,
|
h_contract: purchase.download.contractData.contractTermsHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.trace(
|
|
||||||
"got json",
|
|
||||||
JSON.stringify(await request.json(), undefined, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
const refundResponse = await readSuccessResponseJsonOrThrow(
|
const refundResponse = await readSuccessResponseJsonOrThrow(
|
||||||
request,
|
request,
|
||||||
codecForMerchantOrderRefundPickupResponse(),
|
codecForMerchantOrderRefundPickupResponse(),
|
||||||
@ -777,10 +797,12 @@ export async function abortFailedPayWithRefund(
|
|||||||
purchase.paymentSubmitPending = false;
|
purchase.paymentSubmitPending = false;
|
||||||
purchase.abortStatus = AbortStatus.AbortRefund;
|
purchase.abortStatus = AbortStatus.AbortRefund;
|
||||||
purchase.lastPayError = undefined;
|
purchase.lastPayError = undefined;
|
||||||
purchase.payRetryInfo = initRetryInfo();
|
purchase.payRetryInfo = resetRetryInfo();
|
||||||
await tx.purchases.put(purchase);
|
await tx.purchases.put(purchase);
|
||||||
});
|
});
|
||||||
processPurchaseQueryRefund(ws, proposalId, true).catch((e) => {
|
processPurchaseQueryRefund(ws, proposalId, {
|
||||||
|
forceNow: true,
|
||||||
|
}).catch((e) => {
|
||||||
logger.trace(`error during refund processing after abort pay: ${e}`);
|
logger.trace(`error during refund processing after abort pay: ${e}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,8 @@ import {
|
|||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
import {
|
import {
|
||||||
getRetryDuration,
|
getRetryDuration,
|
||||||
initRetryInfo,
|
resetRetryInfo,
|
||||||
|
RetryInfo,
|
||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
} from "../util/retries.js";
|
} from "../util/retries.js";
|
||||||
import {
|
import {
|
||||||
@ -79,34 +80,15 @@ import { guardOperationException } from "./common.js";
|
|||||||
const logger = new Logger("taler-wallet-core:reserves.ts");
|
const logger = new Logger("taler-wallet-core:reserves.ts");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the retry counter for the reserve
|
* Set up the reserve's retry timeout in preparation for
|
||||||
* and reset the last error.
|
* processing the reserve.
|
||||||
*/
|
*/
|
||||||
async function resetReserveRetry(
|
async function setupReserveRetry(
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const x = await tx.reserves.get(reservePub);
|
|
||||||
if (x) {
|
|
||||||
x.retryInfo = initRetryInfo();
|
|
||||||
delete x.lastError;
|
|
||||||
await tx.reserves.put(x);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increment the retry counter for the reserve and
|
|
||||||
* reset the last eror.
|
|
||||||
*/
|
|
||||||
async function incrementReserveRetry(
|
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
|
options: {
|
||||||
|
reset: boolean;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
@ -117,11 +99,10 @@ async function incrementReserveRetry(
|
|||||||
if (!r) {
|
if (!r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!r.retryInfo) {
|
if (options.reset) {
|
||||||
r.retryInfo = initRetryInfo();
|
r.retryInfo = resetRetryInfo();
|
||||||
} else {
|
} else {
|
||||||
r.retryInfo.retryCounter++;
|
r.retryInfo = RetryInfo.increment(r.retryInfo);
|
||||||
updateRetryInfoTimeout(r.retryInfo);
|
|
||||||
}
|
}
|
||||||
delete r.lastError;
|
delete r.lastError;
|
||||||
await tx.reserves.put(r);
|
await tx.reserves.put(r);
|
||||||
@ -216,7 +197,7 @@ export async function createReserve(
|
|||||||
timestampReserveInfoPosted: undefined,
|
timestampReserveInfoPosted: undefined,
|
||||||
bankInfo,
|
bankInfo,
|
||||||
reserveStatus,
|
reserveStatus,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
currency: req.amount.currency,
|
currency: req.amount.currency,
|
||||||
operationStatus: OperationStatus.Pending,
|
operationStatus: OperationStatus.Pending,
|
||||||
@ -288,7 +269,7 @@ export async function createReserve(
|
|||||||
|
|
||||||
// Asynchronously process the reserve, but return
|
// Asynchronously process the reserve, but return
|
||||||
// to the caller already.
|
// to the caller already.
|
||||||
processReserve(ws, resp.reservePub, true).catch((e) => {
|
processReserve(ws, resp.reservePub, { forceNow: true }).catch((e) => {
|
||||||
logger.error("Processing reserve (after createReserve) failed:", e);
|
logger.error("Processing reserve (after createReserve) failed:", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -316,14 +297,14 @@ export async function forceQueryReserve(
|
|||||||
case ReserveRecordStatus.Dormant:
|
case ReserveRecordStatus.Dormant:
|
||||||
reserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
reserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
||||||
reserve.operationStatus = OperationStatus.Pending;
|
reserve.operationStatus = OperationStatus.Pending;
|
||||||
reserve.retryInfo = initRetryInfo();
|
reserve.retryInfo = resetRetryInfo();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await tx.reserves.put(reserve);
|
await tx.reserves.put(reserve);
|
||||||
});
|
});
|
||||||
await processReserve(ws, reservePub, true);
|
await processReserve(ws, reservePub, { forceNow: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -336,13 +317,15 @@ export async function forceQueryReserve(
|
|||||||
export async function processReserve(
|
export async function processReserve(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return ws.memoProcessReserve.memo(reservePub, async () => {
|
return ws.memoProcessReserve.memo(reservePub, async () => {
|
||||||
const onOpError = (err: TalerErrorDetail): Promise<void> =>
|
const onOpError = (err: TalerErrorDetail): Promise<void> =>
|
||||||
reportReserveError(ws, reservePub, err);
|
reportReserveError(ws, reservePub, err);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processReserveImpl(ws, reservePub, forceNow),
|
() => processReserveImpl(ws, reservePub, options),
|
||||||
onOpError,
|
onOpError,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -409,7 +392,7 @@ async function registerReserveWithBank(
|
|||||||
if (!r.bankInfo) {
|
if (!r.bankInfo) {
|
||||||
throw Error("invariant failed");
|
throw Error("invariant failed");
|
||||||
}
|
}
|
||||||
r.retryInfo = initRetryInfo();
|
r.retryInfo = resetRetryInfo();
|
||||||
await tx.reserves.put(r);
|
await tx.reserves.put(r);
|
||||||
});
|
});
|
||||||
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
|
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
|
||||||
@ -476,7 +459,7 @@ async function processReserveBankStatus(
|
|||||||
r.timestampBankConfirmed = now;
|
r.timestampBankConfirmed = now;
|
||||||
r.reserveStatus = ReserveRecordStatus.BankAborted;
|
r.reserveStatus = ReserveRecordStatus.BankAborted;
|
||||||
r.operationStatus = OperationStatus.Finished;
|
r.operationStatus = OperationStatus.Finished;
|
||||||
r.retryInfo = initRetryInfo();
|
r.retryInfo = resetRetryInfo();
|
||||||
await tx.reserves.put(r);
|
await tx.reserves.put(r);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -513,7 +496,7 @@ async function processReserveBankStatus(
|
|||||||
r.timestampBankConfirmed = now;
|
r.timestampBankConfirmed = now;
|
||||||
r.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
r.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
||||||
r.operationStatus = OperationStatus.Pending;
|
r.operationStatus = OperationStatus.Pending;
|
||||||
r.retryInfo = initRetryInfo();
|
r.retryInfo = resetRetryInfo();
|
||||||
} else {
|
} else {
|
||||||
switch (r.reserveStatus) {
|
switch (r.reserveStatus) {
|
||||||
case ReserveRecordStatus.WaitConfirmBank:
|
case ReserveRecordStatus.WaitConfirmBank:
|
||||||
@ -684,7 +667,7 @@ async function updateReserve(
|
|||||||
reservePub: reserve.reservePub,
|
reservePub: reserve.reservePub,
|
||||||
rawWithdrawalAmount: remainingAmount,
|
rawWithdrawalAmount: remainingAmount,
|
||||||
timestampStart: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
|
timestampStart: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
denomsSel: denomSelectionInfoToState(denomSelInfo),
|
denomsSel: denomSelectionInfoToState(denomSelInfo),
|
||||||
secretSeed: encodeCrock(getRandomBytes(64)),
|
secretSeed: encodeCrock(getRandomBytes(64)),
|
||||||
@ -717,8 +700,12 @@ async function updateReserve(
|
|||||||
async function processReserveImpl(
|
async function processReserveImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const forceNow = options.forceNow ?? false;
|
||||||
|
await setupReserveRetry(ws, reservePub, { reset: forceNow });
|
||||||
const reserve = await ws.db
|
const reserve = await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
reserves: x.reserves,
|
reserves: x.reserves,
|
||||||
@ -732,27 +719,17 @@ async function processReserveImpl(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (forceNow) {
|
|
||||||
await resetReserveRetry(ws, reservePub);
|
|
||||||
} else if (
|
|
||||||
reserve.retryInfo &&
|
|
||||||
!AbsoluteTime.isExpired(reserve.retryInfo.nextRetry)
|
|
||||||
) {
|
|
||||||
logger.trace("processReserve retry not due yet");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await incrementReserveRetry(ws, reservePub);
|
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
|
`Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
|
||||||
);
|
);
|
||||||
switch (reserve.reserveStatus) {
|
switch (reserve.reserveStatus) {
|
||||||
case ReserveRecordStatus.RegisteringBank:
|
case ReserveRecordStatus.RegisteringBank:
|
||||||
await processReserveBankStatus(ws, reservePub);
|
await processReserveBankStatus(ws, reservePub);
|
||||||
return await processReserveImpl(ws, reservePub, true);
|
return await processReserveImpl(ws, reservePub, { forceNow: true });
|
||||||
case ReserveRecordStatus.QueryingStatus:
|
case ReserveRecordStatus.QueryingStatus:
|
||||||
const res = await updateReserve(ws, reservePub);
|
const res = await updateReserve(ws, reservePub);
|
||||||
if (res.ready) {
|
if (res.ready) {
|
||||||
return await processReserveImpl(ws, reservePub, true);
|
return await processReserveImpl(ws, reservePub, { forceNow: true });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ReserveRecordStatus.Dormant:
|
case ReserveRecordStatus.Dormant:
|
||||||
|
@ -43,7 +43,11 @@ import {
|
|||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { j2s } from "@gnu-taler/taler-util";
|
import { j2s } from "@gnu-taler/taler-util";
|
||||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
|
import {
|
||||||
|
resetRetryInfo,
|
||||||
|
RetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
|
} from "../util/retries.js";
|
||||||
import { makeErrorDetail } from "../errors.js";
|
import { makeErrorDetail } from "../errors.js";
|
||||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
@ -127,7 +131,7 @@ export async function prepareTip(
|
|||||||
createdTimestamp: TalerProtocolTimestamp.now(),
|
createdTimestamp: TalerProtocolTimestamp.now(),
|
||||||
merchantTipId: res.merchantTipId,
|
merchantTipId: res.merchantTipId,
|
||||||
tipAmountEffective: selectedDenoms.totalCoinValue,
|
tipAmountEffective: selectedDenoms.totalCoinValue,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: resetRetryInfo(),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
denomsSel: denomSelectionInfoToState(selectedDenoms),
|
denomsSel: denomSelectionInfoToState(selectedDenoms),
|
||||||
pickedUpTimestamp: undefined,
|
pickedUpTimestamp: undefined,
|
||||||
@ -157,10 +161,10 @@ export async function prepareTip(
|
|||||||
return tipStatus;
|
return tipStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function incrementTipRetry(
|
async function reportTipError(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
walletTipId: string,
|
walletTipId: string,
|
||||||
err: TalerErrorDetail | undefined,
|
err: TalerErrorDetail,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
@ -172,10 +176,8 @@ async function incrementTipRetry(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!t.retryInfo) {
|
if (!t.retryInfo) {
|
||||||
return;
|
logger.reportBreak();
|
||||||
}
|
}
|
||||||
t.retryInfo.retryCounter++;
|
|
||||||
updateRetryInfoTimeout(t.retryInfo);
|
|
||||||
t.lastError = err;
|
t.lastError = err;
|
||||||
await tx.tips.put(t);
|
await tx.tips.put(t);
|
||||||
});
|
});
|
||||||
@ -184,15 +186,43 @@ async function incrementTipRetry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupTipRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
walletTipId: string,
|
||||||
|
options: {
|
||||||
|
reset: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
tips: x.tips,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const t = await tx.tips.get(walletTipId);
|
||||||
|
if (!t) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.reset) {
|
||||||
|
t.retryInfo = resetRetryInfo();
|
||||||
|
} else {
|
||||||
|
t.retryInfo = RetryInfo.increment(t.retryInfo);
|
||||||
|
}
|
||||||
|
delete t.lastError;
|
||||||
|
await tx.tips.put(t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function processTip(
|
export async function processTip(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tipId: string,
|
tipId: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
||||||
incrementTipRetry(ws, tipId, e);
|
reportTipError(ws, tipId, e);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processTipImpl(ws, tipId, forceNow),
|
() => processTipImpl(ws, tipId, options),
|
||||||
onOpErr,
|
onOpErr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -208,7 +238,7 @@ async function resetTipRetry(
|
|||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const x = await tx.tips.get(tipId);
|
const x = await tx.tips.get(tipId);
|
||||||
if (x) {
|
if (x) {
|
||||||
x.retryInfo = initRetryInfo();
|
x.retryInfo = resetRetryInfo();
|
||||||
await tx.tips.put(x);
|
await tx.tips.put(x);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -217,8 +247,11 @@ async function resetTipRetry(
|
|||||||
async function processTipImpl(
|
async function processTipImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
walletTipId: string,
|
walletTipId: string,
|
||||||
forceNow: boolean,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const forceNow = options.forceNow ?? false;
|
||||||
if (forceNow) {
|
if (forceNow) {
|
||||||
await resetTipRetry(ws, walletTipId);
|
await resetTipRetry(ws, walletTipId);
|
||||||
}
|
}
|
||||||
@ -293,12 +326,13 @@ async function processTipImpl(
|
|||||||
merchantResp.status === 424)
|
merchantResp.status === 424)
|
||||||
) {
|
) {
|
||||||
logger.trace(`got transient tip error`);
|
logger.trace(`got transient tip error`);
|
||||||
|
// FIXME: wrap in another error code that indicates a transient error
|
||||||
const err = makeErrorDetail(
|
const err = makeErrorDetail(
|
||||||
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
|
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
|
||||||
getHttpResponseErrorDetails(merchantResp),
|
getHttpResponseErrorDetails(merchantResp),
|
||||||
"tip pickup failed (transient)",
|
"tip pickup failed (transient)",
|
||||||
);
|
);
|
||||||
await incrementTipRetry(ws, tipRecord.walletTipId, err);
|
await reportTipError(ws, tipRecord.walletTipId, err);
|
||||||
// FIXME: Maybe we want to signal to the caller that the transient error happened?
|
// FIXME: Maybe we want to signal to the caller that the transient error happened?
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -397,7 +431,7 @@ async function processTipImpl(
|
|||||||
}
|
}
|
||||||
tr.pickedUpTimestamp = TalerProtocolTimestamp.now();
|
tr.pickedUpTimestamp = TalerProtocolTimestamp.now();
|
||||||
tr.lastError = undefined;
|
tr.lastError = undefined;
|
||||||
tr.retryInfo = initRetryInfo();
|
tr.retryInfo = resetRetryInfo();
|
||||||
await tx.tips.put(tr);
|
await tx.tips.put(tr);
|
||||||
for (const cr of newCoinRecords) {
|
for (const cr of newCoinRecords) {
|
||||||
await tx.coins.put(cr);
|
await tx.coins.put(cr);
|
||||||
|
@ -68,7 +68,11 @@ import {
|
|||||||
HttpRequestLibrary,
|
HttpRequestLibrary,
|
||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
} from "../util/http.js";
|
} from "../util/http.js";
|
||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
|
import {
|
||||||
|
resetRetryInfo,
|
||||||
|
RetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
|
} from "../util/retries.js";
|
||||||
import {
|
import {
|
||||||
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
@ -792,10 +796,12 @@ export async function updateWithdrawalDenoms(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function incrementWithdrawalRetry(
|
async function setupWithdrawalRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
err: TalerErrorDetail | undefined,
|
options: {
|
||||||
|
reset: boolean;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
|
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
|
||||||
@ -804,56 +810,61 @@ async function incrementWithdrawalRetry(
|
|||||||
if (!wsr) {
|
if (!wsr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wsr.retryInfo.retryCounter++;
|
if (options.reset) {
|
||||||
updateRetryInfoTimeout(wsr.retryInfo);
|
wsr.retryInfo = resetRetryInfo();
|
||||||
|
} else {
|
||||||
|
wsr.retryInfo = RetryInfo.increment(wsr.retryInfo);
|
||||||
|
}
|
||||||
|
await tx.withdrawalGroups.put(wsr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reportWithdrawalError(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
withdrawalGroupId: string,
|
||||||
|
err: TalerErrorDetail,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const wsr = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
|
if (!wsr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!wsr.retryInfo) {
|
||||||
|
logger.reportBreak();
|
||||||
|
}
|
||||||
wsr.lastError = err;
|
wsr.lastError = err;
|
||||||
await tx.withdrawalGroups.put(wsr);
|
await tx.withdrawalGroups.put(wsr);
|
||||||
});
|
});
|
||||||
if (err) {
|
ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
|
||||||
ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processWithdrawGroup(
|
export async function processWithdrawGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
forceNow = false,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
||||||
incrementWithdrawalRetry(ws, withdrawalGroupId, e);
|
reportWithdrawalError(ws, withdrawalGroupId, e);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
|
() => processWithdrawGroupImpl(ws, withdrawalGroupId, options),
|
||||||
onOpErr,
|
onOpErr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetWithdrawalGroupRetry(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
withdrawalGroupId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const x = await tx.withdrawalGroups.get(withdrawalGroupId);
|
|
||||||
if (x) {
|
|
||||||
x.retryInfo = initRetryInfo();
|
|
||||||
await tx.withdrawalGroups.put(x);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processWithdrawGroupImpl(
|
async function processWithdrawGroupImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
forceNow: boolean,
|
options: {
|
||||||
|
forceNow?: boolean;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const forceNow = options.forceNow ?? false;
|
||||||
logger.trace("processing withdraw group", withdrawalGroupId);
|
logger.trace("processing withdraw group", withdrawalGroupId);
|
||||||
if (forceNow) {
|
await setupWithdrawalRetry(ws, withdrawalGroupId, { reset: forceNow });
|
||||||
await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
|
|
||||||
}
|
|
||||||
const withdrawalGroup = await ws.db
|
const withdrawalGroup = await ws.db
|
||||||
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
|
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
@ -876,7 +887,7 @@ async function processWithdrawGroupImpl(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return await ws.reserveOps.processReserve(ws, reservePub, forceNow);
|
return await ws.reserveOps.processReserve(ws, reservePub, { forceNow });
|
||||||
}
|
}
|
||||||
|
|
||||||
await ws.exchangeOps.updateExchangeFromUrl(
|
await ws.exchangeOps.updateExchangeFromUrl(
|
||||||
@ -948,7 +959,7 @@ async function processWithdrawGroupImpl(
|
|||||||
wg.timestampFinish = TalerProtocolTimestamp.now();
|
wg.timestampFinish = TalerProtocolTimestamp.now();
|
||||||
wg.operationStatus = OperationStatus.Finished;
|
wg.operationStatus = OperationStatus.Finished;
|
||||||
delete wg.lastError;
|
delete wg.lastError;
|
||||||
wg.retryInfo = initRetryInfo();
|
wg.retryInfo = resetRetryInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.withdrawalGroups.put(wg);
|
await tx.withdrawalGroups.put(wg);
|
||||||
|
@ -51,6 +51,8 @@ export interface HttpResponse {
|
|||||||
bytes(): Promise<ArrayBuffer>;
|
bytes(): Promise<ArrayBuffer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
export interface HttpRequestOptions {
|
export interface HttpRequestOptions {
|
||||||
method?: "POST" | "PUT" | "GET";
|
method?: "POST" | "PUT" | "GET";
|
||||||
headers?: { [name: string]: string };
|
headers?: { [name: string]: string };
|
||||||
|
@ -82,7 +82,7 @@ export function getRetryDuration(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initRetryInfo(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
|
export function resetRetryInfo(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
|
||||||
const now = AbsoluteTime.now();
|
const now = AbsoluteTime.now();
|
||||||
const info = {
|
const info = {
|
||||||
firstTry: now,
|
firstTry: now,
|
||||||
@ -99,7 +99,7 @@ export namespace RetryInfo {
|
|||||||
p: RetryPolicy = defaultRetryPolicy,
|
p: RetryPolicy = defaultRetryPolicy,
|
||||||
) {
|
) {
|
||||||
if (!r) {
|
if (!r) {
|
||||||
return initRetryInfo(p);
|
return resetRetryInfo(p);
|
||||||
}
|
}
|
||||||
const r2 = { ...r };
|
const r2 = { ...r };
|
||||||
r2.retryCounter++;
|
r2.retryCounter++;
|
||||||
|
@ -238,51 +238,41 @@ async function processOnePendingOperation(
|
|||||||
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 PendingTaskType.ExchangeUpdate:
|
case PendingTaskType.ExchangeUpdate:
|
||||||
await updateExchangeFromUrl(
|
await updateExchangeFromUrl(ws, pending.exchangeBaseUrl, {
|
||||||
ws,
|
|
||||||
pending.exchangeBaseUrl,
|
|
||||||
undefined,
|
|
||||||
forceNow,
|
forceNow,
|
||||||
);
|
});
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.Refresh:
|
case PendingTaskType.Refresh:
|
||||||
await processRefreshGroup(ws, pending.refreshGroupId, forceNow);
|
await processRefreshGroup(ws, pending.refreshGroupId, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.Reserve:
|
case PendingTaskType.Reserve:
|
||||||
await processReserve(ws, pending.reservePub, forceNow);
|
await processReserve(ws, pending.reservePub, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.Withdraw:
|
case PendingTaskType.Withdraw:
|
||||||
await processWithdrawGroup(ws, pending.withdrawalGroupId, forceNow);
|
await processWithdrawGroup(ws, pending.withdrawalGroupId, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.ProposalDownload:
|
case PendingTaskType.ProposalDownload:
|
||||||
await processDownloadProposal(ws, pending.proposalId, forceNow);
|
await processDownloadProposal(ws, pending.proposalId, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.TipPickup:
|
case PendingTaskType.TipPickup:
|
||||||
await processTip(ws, pending.tipId, forceNow);
|
await processTip(ws, pending.tipId, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.Pay:
|
case PendingTaskType.Pay:
|
||||||
await processPurchasePay(ws, pending.proposalId, forceNow);
|
await processPurchasePay(ws, pending.proposalId, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.RefundQuery:
|
case PendingTaskType.RefundQuery:
|
||||||
await processPurchaseQueryRefund(ws, pending.proposalId, forceNow);
|
await processPurchaseQueryRefund(ws, pending.proposalId, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.Recoup:
|
case PendingTaskType.Recoup:
|
||||||
await processRecoupGroup(ws, pending.recoupGroupId, forceNow);
|
await processRecoupGroup(ws, pending.recoupGroupId, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.ExchangeCheckRefresh:
|
case PendingTaskType.ExchangeCheckRefresh:
|
||||||
await autoRefresh(ws, pending.exchangeBaseUrl);
|
await autoRefresh(ws, pending.exchangeBaseUrl);
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.Deposit: {
|
case PendingTaskType.Deposit: {
|
||||||
const cts = CancellationToken.create();
|
await processDepositGroup(ws, pending.depositGroupId, {
|
||||||
ws.taskCancellationSourceForDeposit = cts;
|
forceNow,
|
||||||
try {
|
});
|
||||||
await processDepositGroup(ws, pending.depositGroupId, {
|
|
||||||
cancellationToken: cts.token,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
cts.dispose();
|
|
||||||
delete ws.taskCancellationSourceForDeposit;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PendingTaskType.Backup:
|
case PendingTaskType.Backup:
|
||||||
@ -497,11 +487,8 @@ async function getExchangeTos(
|
|||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
acceptedFormat?: string[],
|
acceptedFormat?: string[],
|
||||||
): Promise<GetExchangeTosResult> {
|
): Promise<GetExchangeTosResult> {
|
||||||
const { exchangeDetails } = await updateExchangeFromUrl(
|
// FIXME: download ToS in acceptable format if passed!
|
||||||
ws,
|
const { exchangeDetails } = await updateExchangeFromUrl(ws, exchangeBaseUrl);
|
||||||
exchangeBaseUrl,
|
|
||||||
acceptedFormat,
|
|
||||||
);
|
|
||||||
const content = exchangeDetails.termsOfServiceText;
|
const content = exchangeDetails.termsOfServiceText;
|
||||||
const currentEtag = exchangeDetails.termsOfServiceLastEtag;
|
const currentEtag = exchangeDetails.termsOfServiceLastEtag;
|
||||||
const contentType = exchangeDetails.termsOfServiceContentType;
|
const contentType = exchangeDetails.termsOfServiceContentType;
|
||||||
@ -802,12 +789,9 @@ async function dispatchRequestInternal(
|
|||||||
}
|
}
|
||||||
case "addExchange": {
|
case "addExchange": {
|
||||||
const req = codecForAddExchangeRequest().decode(payload);
|
const req = codecForAddExchangeRequest().decode(payload);
|
||||||
await updateExchangeFromUrl(
|
await updateExchangeFromUrl(ws, req.exchangeBaseUrl, {
|
||||||
ws,
|
forceNow: req.forceUpdate,
|
||||||
req.exchangeBaseUrl,
|
});
|
||||||
undefined,
|
|
||||||
req.forceUpdate,
|
|
||||||
);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
case "listExchanges": {
|
case "listExchanges": {
|
||||||
@ -919,11 +903,11 @@ async function dispatchRequestInternal(
|
|||||||
RefreshReason.Manual,
|
RefreshReason.Manual,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
processRefreshGroup(ws, refreshGroupId.refreshGroupId, true).catch(
|
processRefreshGroup(ws, refreshGroupId.refreshGroupId, {
|
||||||
(x) => {
|
forceNow: true,
|
||||||
logger.error(x);
|
}).catch((x) => {
|
||||||
},
|
logger.error(x);
|
||||||
);
|
});
|
||||||
return {
|
return {
|
||||||
refreshGroupId,
|
refreshGroupId,
|
||||||
};
|
};
|
||||||
@ -1170,7 +1154,7 @@ class InternalWalletStateImpl implements InternalWalletState {
|
|||||||
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
|
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
|
||||||
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
|
||||||
cryptoApi: TalerCryptoInterface;
|
cryptoApi: TalerCryptoInterface;
|
||||||
cryptoDispatcher: CryptoDispatcher;
|
cryptoDispatcher: CryptoDispatcher;
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Balance, parsePaytoUri } from "@gnu-taler/taler-util";
|
import { Balance, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||||
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
import type { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits.js";
|
||||||
import { createExample } from "../test-utils.js";
|
import { createExample } from "../test-utils.js";
|
||||||
import { View as TestedComponent } from "./DepositPage.js";
|
import { View as TestedComponent } from "./DepositPage.js";
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ export default {
|
|||||||
argTypes: {},
|
argTypes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function alwaysReturnFeeToOne(): Promise<DepositFee> {
|
async function alwaysReturnFeeToOne(): Promise<DepositGroupFees> {
|
||||||
const fee = {
|
const fee = {
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
value: 1,
|
value: 1,
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
Balance,
|
Balance,
|
||||||
PaytoUri,
|
PaytoUri,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
import { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { Loading } from "../components/Loading.js";
|
import { Loading } from "../components/Loading.js";
|
||||||
@ -68,7 +68,7 @@ export function DepositPage({ currency, onCancel, onSuccess }: Props): VNode {
|
|||||||
async function getFeeForAmount(
|
async function getFeeForAmount(
|
||||||
p: PaytoUri,
|
p: PaytoUri,
|
||||||
a: AmountJson,
|
a: AmountJson,
|
||||||
): Promise<DepositFee> {
|
): Promise<DepositGroupFees> {
|
||||||
const account = `payto://${p.targetType}/${p.targetPath}`;
|
const account = `payto://${p.targetType}/${p.targetPath}`;
|
||||||
const amount = Amounts.stringify(a);
|
const amount = Amounts.stringify(a);
|
||||||
return await wxApi.getFeeForDeposit(account, amount);
|
return await wxApi.getFeeForDeposit(account, amount);
|
||||||
@ -106,7 +106,7 @@ interface ViewProps {
|
|||||||
onCalculateFee: (
|
onCalculateFee: (
|
||||||
account: PaytoUri,
|
account: PaytoUri,
|
||||||
amount: AmountJson,
|
amount: AmountJson,
|
||||||
) => Promise<DepositFee>;
|
) => Promise<DepositGroupFees>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = NoBalanceState | NoAccountsState | DepositState;
|
type State = NoBalanceState | NoAccountsState | DepositState;
|
||||||
@ -135,12 +135,12 @@ export function useComponentState(
|
|||||||
onCalculateFee: (
|
onCalculateFee: (
|
||||||
account: PaytoUri,
|
account: PaytoUri,
|
||||||
amount: AmountJson,
|
amount: AmountJson,
|
||||||
) => Promise<DepositFee>,
|
) => Promise<DepositGroupFees>,
|
||||||
): State {
|
): State {
|
||||||
const accountMap = createLabelsForBankAccount(accounts);
|
const accountMap = createLabelsForBankAccount(accounts);
|
||||||
const [accountIdx, setAccountIdx] = useState(0);
|
const [accountIdx, setAccountIdx] = useState(0);
|
||||||
const [amount, setAmount] = useState<number | undefined>(undefined);
|
const [amount, setAmount] = useState<number | undefined>(undefined);
|
||||||
const [fee, setFee] = useState<DepositFee | undefined>(undefined);
|
const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined);
|
||||||
function updateAmount(num: number | undefined) {
|
function updateAmount(num: number | undefined) {
|
||||||
setAmount(num);
|
setAmount(num);
|
||||||
setFee(undefined);
|
setFee(undefined);
|
||||||
|
@ -59,7 +59,7 @@ import {
|
|||||||
RemoveBackupProviderRequest,
|
RemoveBackupProviderRequest,
|
||||||
TalerError,
|
TalerError,
|
||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
import type { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
import type { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
||||||
import type { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
|
import type { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
|
||||||
import { platform, MessageFromBackend } from "./platform/api.js";
|
import { platform, MessageFromBackend } from "./platform/api.js";
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ export function resetDb(): Promise<void> {
|
|||||||
export function getFeeForDeposit(
|
export function getFeeForDeposit(
|
||||||
depositPaytoUri: string,
|
depositPaytoUri: string,
|
||||||
amount: AmountString,
|
amount: AmountString,
|
||||||
): Promise<DepositFee> {
|
): Promise<DepositGroupFees> {
|
||||||
return callBackend("getFeeForDeposit", {
|
return callBackend("getFeeForDeposit", {
|
||||||
depositPaytoUri,
|
depositPaytoUri,
|
||||||
amount,
|
amount,
|
||||||
|
Loading…
Reference in New Issue
Block a user