wallet-core/packages/taler-wallet-core/src/wallet.ts

1637 lines
46 KiB
TypeScript
Raw Normal View History

2015-12-25 22:42:14 +01:00
/*
This file is part of GNU Taler
2019-11-30 00:36:20 +01:00
(C) 2015-2019 GNUnet e.V.
2015-12-25 22:42:14 +01:00
GNU Taler is free software; you can redistribute it and/or modify it under the
2015-12-25 22:42:14 +01:00
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
2015-12-25 22:42:14 +01:00
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
2015-12-25 22:42:14 +01:00
*/
2016-01-05 15:42:46 +01:00
/**
* High-level wallet operations that should be indepentent from the underlying
* browser extension interface.
*/
2017-05-24 16:52:00 +02:00
/**
* Imports.
*/
2021-05-12 16:18:32 +02:00
import {
AbsoluteTime,
AmountJson,
Amounts,
codecForAbortPayWithRefundRequest,
codecForAcceptBankIntegratedWithdrawalRequest,
codecForAcceptExchangeTosRequest,
codecForAcceptManualWithdrawalRequet,
codecForAcceptPeerPullPaymentRequest,
codecForAcceptPeerPushPaymentRequest,
codecForAcceptTipRequest,
codecForAddExchangeRequest,
codecForAddKnownBankAccounts,
codecForAny,
codecForApplyRefundFromPurchaseIdRequest,
codecForApplyRefundRequest,
codecForCheckPeerPullPaymentRequest,
codecForCheckPeerPushPaymentRequest,
codecForConfirmPayRequest,
codecForCreateDepositGroupRequest,
codecForDeleteTransactionRequest,
codecForForceRefreshRequest,
codecForForgetKnownBankAccounts,
codecForGetContractTermsDetails,
codecForGetExchangeTosRequest,
codecForGetExchangeWithdrawalInfo,
codecForGetFeeForDeposit,
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
codecForImportDbRequest,
codecForInitiatePeerPullPaymentRequest,
codecForInitiatePeerPushPaymentRequest,
codecForIntegrationTestArgs,
codecForListKnownBankAccounts,
2022-05-03 05:16:03 +02:00
codecForPrepareDepositRequest,
codecForPreparePayRequest,
codecForPrepareRefundRequest,
codecForPrepareTipRequest,
codecForRetryTransactionRequest,
codecForSetCoinSuspendedRequest,
codecForSetWalletDeviceIdRequest,
codecForTestPayArgs,
codecForTrackDepositGroupRequest,
codecForTransactionByIdRequest,
codecForTransactionsRequest,
codecForWithdrawFakebankRequest,
codecForWithdrawTestBalance,
CoinDumpJson,
CoreApiResponse,
DenominationInfo,
DenomOperationMap,
Duration,
durationFromSpec,
durationMin,
ExchangeFullDetails,
ExchangeListItem,
ExchangesListResponse,
FeeDescription,
GetExchangeTosResult,
j2s,
KnownBankAccounts,
KnownBankAccountsInfo,
Logger,
ManualWithdrawalDetails,
NotificationType,
parsePaytoUri,
RefreshReason,
TalerErrorCode,
codecForApplyDevExperiment,
URL,
WalletCoreVersion,
WalletNotification,
codecForSetDevModeRequest,
2021-05-12 16:18:32 +02:00
} from "@gnu-taler/taler-util";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
2022-03-23 21:24:23 +01:00
import {
CryptoDispatcher,
CryptoWorkerFactory,
2022-03-23 21:24:23 +01:00
} from "./crypto/workers/cryptoDispatcher.js";
import { clearDatabase } from "./db-utils.js";
import {
AuditorTrustRecord,
CoinSourceType,
CoinStatus,
ConfigRecordKey,
DenominationRecord,
exportDb,
importDb,
WalletStoresV1,
} from "./db.js";
import { applyDevExperiment, maybeInitDevMode, setDevMode } from "./dev-experiments.js";
import { getErrorDetailFromException, TalerError } from "./errors.js";
import {
ActiveLongpollInfo,
ExchangeOperations,
InternalWalletState,
MerchantInfo,
MerchantOperations,
NotificationListener,
RecoupOperations,
RefreshOperations,
} from "./internal-wallet-state.js";
import { exportBackup } from "./operations/backup/export.js";
import {
2021-02-08 15:38:34 +01:00
addBackupProvider,
codecForAddBackupProviderRequest,
codecForRemoveBackupProvider,
2021-07-07 18:28:02 +02:00
codecForRunBackupCycle,
2021-02-08 15:38:34 +01:00
getBackupInfo,
getBackupRecovery,
importBackupPlain,
2021-02-08 15:38:34 +01:00
loadBackupRecovery,
processBackupForProvider,
removeBackupProvider,
runBackupCycle,
} from "./operations/backup/index.js";
import { setWalletDeviceId } from "./operations/backup/state.js";
import { getBalances } from "./operations/balance.js";
import { runOperationWithErrorReporting } from "./operations/common.js";
2021-02-08 15:38:34 +01:00
import {
createDepositGroup,
2021-12-23 19:17:36 +01:00
getFeeForDeposit,
2022-05-03 05:16:03 +02:00
prepareDepositGroup,
2021-02-08 15:38:34 +01:00
processDepositGroup,
trackDepositGroup,
} from "./operations/deposits.js";
2021-02-08 15:38:34 +01:00
import {
acceptExchangeTermsOfService,
downloadTosFromAcceptedFormat,
2021-06-09 15:14:17 +02:00
getExchangeDetails,
getExchangeRequestTimeout,
getExchangeTrust,
provideExchangeRecordInTx,
updateExchangeFromUrl,
2022-09-05 18:12:30 +02:00
updateExchangeFromUrlHandler,
updateExchangeTermsOfService,
} from "./operations/exchanges.js";
import { getMerchantInfo } from "./operations/merchants.js";
import {
abortFailedPayWithRefund,
applyRefund,
applyRefundFromPurchaseId,
confirmPay,
getContractTermsDetails,
2021-02-08 15:38:34 +01:00
preparePayForUri,
prepareRefund,
processPurchase,
} from "./operations/pay-merchant.js";
import {
acceptPeerPullPayment,
acceptPeerPushPayment,
checkPeerPullPayment,
checkPeerPushPayment,
initiatePeerRequestForPay,
initiatePeerToPeerPush,
} from "./operations/pay-peer.js";
import { getPendingOperations } from "./operations/pending.js";
2022-09-05 18:12:30 +02:00
import {
createRecoupGroup,
processRecoupGroup,
processRecoupGroupHandler,
} from "./operations/recoup.js";
2021-02-08 15:38:34 +01:00
import {
autoRefresh,
createRefreshGroup,
processRefreshGroup,
} from "./operations/refresh.js";
2021-02-08 15:38:34 +01:00
import {
runIntegrationTest,
testPay,
withdrawTestBalance,
} from "./operations/testing.js";
import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
import {
deleteTransaction,
getTransactionById,
getTransactions,
retryTransaction,
} from "./operations/transactions.js";
2021-02-08 15:38:34 +01:00
import {
2022-08-09 15:00:45 +02:00
acceptWithdrawalFromUri,
createManualWithdrawal,
2021-02-08 15:38:34 +01:00
getExchangeWithdrawalInfo,
getWithdrawalDetailsForUri,
processWithdrawalGroup,
} from "./operations/withdraw.js";
import { PendingTaskInfo, PendingTaskType } from "./pending-types.js";
import { assertUnreachable } from "./util/assertUnreachable.js";
import {
createTimeline,
selectBestForOverlappingDenominations,
selectMinimumFee,
} from "./util/denominations.js";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "./util/http.js";
import { checkDbInvariant } from "./util/invariants.js";
import {
AsyncCondition,
OpenedPromise,
openPromise,
} from "./util/promiseUtils.js";
2022-01-13 12:08:31 +01:00
import { DbAccess, GetReadWriteAccess } from "./util/query.js";
import { OperationAttemptResult } from "./util/retries.js";
import { TimerAPI, TimerGroup } from "./util/timer.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
WALLET_MERCHANT_PROTOCOL_VERSION,
} from "./versions.js";
import { WalletCoreApiClient } from "./wallet-api-types.js";
const builtinAuditors: AuditorTrustRecord[] = [
{
currency: "KUDOS",
auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0",
auditorBaseUrl: "https://auditor.demo.taler.net/",
uids: ["5P25XF8TVQP9AW6VYGY2KV47WT5Y3ZXFSJAA570GJPX5SVJXKBVG"],
},
];
2022-08-16 17:59:17 +02:00
const builtinExchanges: string[] = ["https://exchange.demo.taler.net/"];
2019-11-21 23:09:43 +01:00
const logger = new Logger("wallet.ts");
async function getWithdrawalDetailsForAmount(
ws: InternalWalletState,
exchangeBaseUrl: string,
amount: AmountJson,
restrictAge: number | undefined,
): Promise<ManualWithdrawalDetails> {
2022-09-05 18:12:30 +02:00
const wi = await getExchangeWithdrawalInfo(
ws,
exchangeBaseUrl,
amount,
restrictAge,
);
const paytoUris = wi.exchangeDetails.wireInfo.accounts.map(
(x) => x.payto_uri,
);
if (!paytoUris) {
throw Error("exchange is in invalid state");
}
return {
amountRaw: Amounts.stringify(amount),
amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
paytoUris,
tosAccepted: wi.termsOfServiceAccepted,
};
}
/**
2022-09-05 18:12:30 +02:00
* Call the right handler for a pending operation without doing
* any special error handling.
*/
2022-09-05 18:12:30 +02:00
async function callOperationHandler(
ws: InternalWalletState,
pending: PendingTaskInfo,
forceNow = false,
2022-09-16 17:21:54 +02:00
): Promise<OperationAttemptResult> {
switch (pending.type) {
case PendingTaskType.ExchangeUpdate:
2022-09-05 18:12:30 +02:00
return await updateExchangeFromUrlHandler(ws, pending.exchangeBaseUrl, {
2021-10-14 11:36:43 +02:00
forceNow,
});
case PendingTaskType.Refresh:
2022-09-05 18:12:30 +02:00
return await processRefreshGroup(ws, pending.refreshGroupId, {
forceNow,
});
case PendingTaskType.Withdraw:
2022-09-13 15:28:34 +02:00
return await processWithdrawalGroup(ws, pending.withdrawalGroupId, {
forceNow,
});
case PendingTaskType.TipPickup:
2022-09-05 18:12:30 +02:00
return await processTip(ws, pending.tipId, { forceNow });
case PendingTaskType.Purchase:
return await processPurchase(ws, pending.proposalId, { forceNow });
case PendingTaskType.Recoup:
2022-09-05 18:12:30 +02:00
return await processRecoupGroupHandler(ws, pending.recoupGroupId, {
forceNow,
});
case PendingTaskType.ExchangeCheckRefresh:
2022-09-05 18:12:30 +02:00
return await autoRefresh(ws, pending.exchangeBaseUrl);
2022-03-28 23:59:16 +02:00
case PendingTaskType.Deposit: {
2022-09-05 18:12:30 +02:00
return await processDepositGroup(ws, pending.depositGroupId, {
forceNow,
});
2022-03-28 23:59:16 +02:00
}
case PendingTaskType.Backup:
2022-09-05 18:12:30 +02:00
return await processBackupForProvider(ws, pending.backupProviderBaseUrl);
default:
2022-09-05 18:12:30 +02:00
return assertUnreachable(pending);
}
2022-09-13 15:28:34 +02:00
throw Error(`not reached ${pending.type}`);
2022-09-05 18:12:30 +02:00
}
/**
* Process pending operations.
*/
export async function runPending(
ws: InternalWalletState,
forceNow = false,
): Promise<void> {
const pendingOpsResponse = await getPendingOperations(ws);
for (const p of pendingOpsResponse.pendingOperations) {
2022-03-18 15:32:41 +01:00
if (!forceNow && !AbsoluteTime.isExpired(p.timestampDue)) {
continue;
2020-07-11 09:56:07 +02:00
}
await runOperationWithErrorReporting(ws, p.id, async () => {
logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
return await callOperationHandler(ws, p, forceNow);
});
2019-11-30 00:36:20 +01:00
}
}
2019-11-21 23:09:43 +01:00
export interface RetryLoopOpts {
/**
* Stop when the number of retries is exceeded for any pending
* operation.
*/
maxRetries?: number;
/**
* Stop the retry loop when all lifeness-giving pending operations
* are done.
*
* Defaults to false.
*/
stopWhenDone?: boolean;
}
2019-12-05 19:38:19 +01:00
export interface TaskLoopResult {
/**
* Was the maximum number of retries exceeded in a task?
*/
retriesExceeded: boolean;
}
/**
* Main retry loop of the wallet.
*
* Looks up pending operations from the wallet, runs them, repeat.
*/
async function runTaskLoop(
ws: InternalWalletState,
opts: RetryLoopOpts = {},
): Promise<TaskLoopResult> {
logger.info(`running task loop opts=${j2s(opts)}`);
let retriesExceeded = false;
for (let iteration = 0; !ws.stopped; iteration++) {
const pending = await getPendingOperations(ws);
logger.trace(`pending operations: ${j2s(pending)}`);
let numGivingLiveness = 0;
let numDue = 0;
2022-03-18 15:32:41 +01:00
let minDue: AbsoluteTime = AbsoluteTime.never();
for (const p of pending.pendingOperations) {
const maxRetries = opts.maxRetries;
if (maxRetries && p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
retriesExceeded = true;
logger.warn(
`skipping, as ${maxRetries} retries are exceeded in an operation of type ${p.type}`,
);
continue;
}
if (p.givesLifeness) {
numGivingLiveness++;
}
if (!p.isDue) {
continue;
}
minDue = AbsoluteTime.min(minDue, p.timestampDue);
numDue++;
}
if (opts.stopWhenDone && numGivingLiveness === 0 && iteration !== 0) {
logger.warn(`stopping, as no pending operations have lifeness`);
return {
retriesExceeded,
};
}
// Make sure that we run tasks that don't give lifeness at least
// one time.
if (iteration !== 0 && numDue === 0) {
// We've executed pending, due operations at least one.
// Now we don't have any more operations available,
// and need to wait.
// Wait for at most 5 seconds to the next check.
const dt = durationMin(
durationFromSpec({
seconds: 5,
}),
2022-03-18 15:32:41 +01:00
Duration.getRemaining(minDue),
);
logger.trace(`waiting for at most ${dt.d_ms} ms`);
const timeout = ws.timerGroup.resolveAfter(dt);
ws.notify({
type: NotificationType.WaitingForRetry,
numGivingLiveness,
numPending: pending.pendingOperations.length,
});
// Wait until either the timeout, or we are notified (via the latch)
// that more work might be available.
await Promise.race([timeout, ws.latch.wait()]);
} else {
logger.trace(
`running ${pending.pendingOperations.length} pending operations`,
);
2020-09-03 17:08:26 +02:00
for (const p of pending.pendingOperations) {
2022-03-18 15:32:41 +01:00
if (!AbsoluteTime.isExpired(p.timestampDue)) {
continue;
2020-09-03 17:08:26 +02:00
}
await runOperationWithErrorReporting(ws, p.id, async () => {
logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
return await callOperationHandler(ws, p);
});
ws.notify({
type: NotificationType.PendingOperationProcessed,
});
2019-11-21 23:09:43 +01:00
}
}
}
logger.trace("exiting wallet retry loop");
return {
retriesExceeded,
};
}
/**
* Insert the hard-coded defaults for exchanges, coins and
* auditors into the database, unless these defaults have
* already been applied.
*/
async function fillDefaults(ws: InternalWalletState): Promise<void> {
await ws.db
.mktx((x) => [x.config, x.auditorTrust, x.exchanges, x.exchangeDetails])
.runReadWrite(async (tx) => {
const appliedRec = await tx.config.get("currencyDefaultsApplied");
let alreadyApplied = appliedRec ? !!appliedRec.value : false;
if (alreadyApplied) {
2022-10-07 12:43:17 +02:00
logger.trace("defaults already applied");
return;
}
2022-10-07 12:43:17 +02:00
logger.info("importing default exchanges and auditors");
for (const c of builtinAuditors) {
await tx.auditorTrust.put(c);
}
for (const baseUrl of builtinExchanges) {
const now = AbsoluteTime.now();
provideExchangeRecordInTx(ws, tx, baseUrl, now);
}
await tx.config.put({
key: ConfigRecordKey.CurrencyDefaultsApplied,
value: true,
});
});
}
async function getExchangeTos(
ws: InternalWalletState,
exchangeBaseUrl: string,
2021-10-13 19:26:18 +02:00
acceptedFormat?: string[],
): Promise<GetExchangeTosResult> {
// FIXME: download ToS in acceptable format if passed!
const { exchangeDetails } = await updateExchangeFromUrl(ws, exchangeBaseUrl);
2021-10-13 19:26:18 +02:00
const content = exchangeDetails.termsOfServiceText;
const currentEtag = exchangeDetails.termsOfServiceLastEtag;
2021-10-13 19:26:18 +02:00
const contentType = exchangeDetails.termsOfServiceContentType;
2021-10-14 11:36:43 +02:00
if (
content === undefined ||
currentEtag === undefined ||
contentType === undefined
) {
throw Error("exchange is in invalid state");
}
if (
acceptedFormat &&
acceptedFormat.findIndex((f) => f === contentType) !== -1
) {
return {
acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
currentEtag,
content,
contentType,
};
}
const tosDownload = await downloadTosFromAcceptedFormat(
ws,
exchangeBaseUrl,
getExchangeRequestTimeout(),
acceptedFormat,
);
if (tosDownload.tosContentType === contentType) {
return {
acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
currentEtag,
content,
contentType,
};
}
await updateExchangeTermsOfService(ws, exchangeBaseUrl, tosDownload);
return {
acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
currentEtag: tosDownload.tosEtag,
content: tosDownload.tosText,
contentType: tosDownload.tosContentType,
};
}
2019-11-30 00:36:20 +01:00
2022-08-09 15:00:45 +02:00
/**
* List bank accounts known to the wallet from
* previous withdrawals.
*/
2021-12-23 19:17:36 +01:00
async function listKnownBankAccounts(
ws: InternalWalletState,
currency?: string,
): Promise<KnownBankAccounts> {
2022-09-23 20:17:29 +02:00
const accounts: KnownBankAccountsInfo[] = [];
2021-12-23 19:17:36 +01:00
await ws.db
2022-09-23 20:17:29 +02:00
.mktx((x) => [x.bankAccounts])
2021-12-23 19:17:36 +01:00
.runReadOnly(async (tx) => {
2022-09-23 20:17:29 +02:00
const knownAccounts = await tx.bankAccounts.iter().toArray();
for (const r of knownAccounts) {
if (currency && currency !== r.currency) {
2022-01-13 12:08:31 +01:00
continue;
2021-12-23 19:17:36 +01:00
}
2022-09-23 20:17:29 +02:00
const payto = parsePaytoUri(r.uri);
if (payto) {
accounts.push({
uri: payto,
alias: r.alias,
kyc_completed: r.kycCompleted,
2022-09-23 20:17:29 +02:00
currency: r.currency,
});
2021-12-23 19:17:36 +01:00
}
}
2022-01-13 12:08:31 +01:00
});
return { accounts };
2021-12-23 19:17:36 +01:00
}
2022-09-23 20:17:29 +02:00
/**
*/
async function addKnownBankAccounts(
ws: InternalWalletState,
payto: string,
alias: string,
currency: string,
): Promise<void> {
await ws.db
.mktx((x) => [x.bankAccounts])
.runReadWrite(async (tx) => {
tx.bankAccounts.put({
uri: payto,
alias: alias,
currency: currency,
kycCompleted: false,
2022-09-23 20:17:29 +02:00
});
});
return;
}
/**
*/
async function forgetKnownBankAccounts(
ws: InternalWalletState,
payto: string,
): Promise<void> {
await ws.db
.mktx((x) => [x.bankAccounts])
.runReadWrite(async (tx) => {
const account = await tx.bankAccounts.get(payto);
if (!account) {
throw Error(`account not found: ${payto}`);
}
tx.bankAccounts.delete(account.uri);
});
return;
}
async function getExchanges(
ws: InternalWalletState,
): Promise<ExchangesListResponse> {
const exchanges: ExchangeListItem[] = [];
await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
.runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) {
const dp = r.detailsPointer;
if (!dp) {
continue;
}
2021-10-13 19:26:18 +02:00
const { currency } = dp;
const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
if (!exchangeDetails) {
continue;
}
const denominations = await tx.denominations.indexes.byExchangeBaseUrl
.iter(r.baseUrl)
.toArray();
if (!denominations) {
continue;
}
exchanges.push({
exchangeBaseUrl: r.baseUrl,
currency,
tos: {
acceptedVersion: exchangeDetails.termsOfServiceAcceptedEtag,
currentVersion: exchangeDetails.termsOfServiceLastEtag,
contentType: exchangeDetails.termsOfServiceContentType,
content: exchangeDetails.termsOfServiceText,
},
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
});
}
});
return { exchanges };
}
async function getExchangeDetailedInfo(
ws: InternalWalletState,
exchangeBaseurl: string,
): Promise<ExchangeFullDetails> {
//TODO: should we use the forceUpdate parameter?
const exchange = await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
.runReadOnly(async (tx) => {
2022-09-05 18:12:30 +02:00
const ex = await tx.exchanges.get(exchangeBaseurl);
const dp = ex?.detailsPointer;
if (!dp) {
return;
}
const { currency } = dp;
const exchangeDetails = await getExchangeDetails(tx, ex.baseUrl);
if (!exchangeDetails) {
return;
}
const denominationRecords =
await tx.denominations.indexes.byExchangeBaseUrl
.iter(ex.baseUrl)
.toArray();
if (!denominationRecords) {
return;
}
const denominations: DenominationInfo[] = denominationRecords.map((x) =>
DenominationRecord.toDenomInfo(x),
);
return {
2022-09-12 15:57:13 +02:00
info: {
exchangeBaseUrl: ex.baseUrl,
currency,
tos: {
acceptedVersion: exchangeDetails.termsOfServiceAcceptedEtag,
currentVersion: exchangeDetails.termsOfServiceLastEtag,
contentType: exchangeDetails.termsOfServiceContentType,
content: exchangeDetails.termsOfServiceText,
},
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
auditors: exchangeDetails.auditors,
wireInfo: exchangeDetails.wireInfo,
globalFees: exchangeDetails.globalFees,
},
denominations,
2022-09-05 18:12:30 +02:00
};
});
2022-09-12 15:57:13 +02:00
if (!exchange) {
2022-09-05 18:12:30 +02:00
throw Error(`exchange with base url "${exchangeBaseurl}" not found`);
}
2022-09-12 15:57:13 +02:00
const denoms = exchange.denominations.map((d) => ({
...d,
group: Amounts.stringifyValue(d.value),
}));
const denomFees: DenomOperationMap<FeeDescription[]> = {
deposit: createTimeline(
denoms,
"denomPubHash",
"stampStart",
2022-09-12 15:57:13 +02:00
"stampExpireDeposit",
"feeDeposit",
"group",
selectBestForOverlappingDenominations,
2022-09-12 15:57:13 +02:00
),
refresh: createTimeline(
denoms,
"denomPubHash",
"stampStart",
2022-09-12 15:57:13 +02:00
"stampExpireWithdraw",
"feeRefresh",
"group",
selectBestForOverlappingDenominations,
2022-09-12 15:57:13 +02:00
),
refund: createTimeline(
denoms,
"denomPubHash",
"stampStart",
2022-09-12 15:57:13 +02:00
"stampExpireWithdraw",
"feeRefund",
"group",
selectBestForOverlappingDenominations,
2022-09-12 15:57:13 +02:00
),
withdraw: createTimeline(
denoms,
"denomPubHash",
"stampStart",
2022-09-12 15:57:13 +02:00
"stampExpireWithdraw",
"feeWithdraw",
"group",
selectBestForOverlappingDenominations,
2022-09-12 15:57:13 +02:00
),
};
const transferFees = Object.entries(
exchange.info.wireInfo.feesForType,
).reduce((prev, [wireType, infoForType]) => {
const feesByGroup = [
...infoForType.map((w) => ({
...w,
fee: w.closingFee,
group: "closing",
})),
...infoForType.map((w) => ({ ...w, fee: w.wadFee, group: "wad" })),
...infoForType.map((w) => ({ ...w, fee: w.wireFee, group: "wire" })),
];
prev[wireType] = createTimeline(
feesByGroup,
"sig",
"startStamp",
"endStamp",
"fee",
"group",
selectMinimumFee,
);
return prev;
}, {} as Record<string, FeeDescription[]>);
const globalFeesByGroup = [
...exchange.info.globalFees.map((w) => ({
...w,
fee: w.accountFee,
group: "account",
})),
...exchange.info.globalFees.map((w) => ({
...w,
fee: w.historyFee,
group: "history",
})),
...exchange.info.globalFees.map((w) => ({
...w,
fee: w.kycFee,
group: "kyc",
})),
...exchange.info.globalFees.map((w) => ({
...w,
fee: w.purseFee,
group: "purse",
})),
];
const globalFees = createTimeline(
globalFeesByGroup,
"signature",
"startDate",
"endDate",
"fee",
"group",
selectMinimumFee,
);
2022-09-12 15:57:13 +02:00
return {
...exchange.info,
denomFees,
transferFees,
globalFees,
2022-09-12 15:57:13 +02:00
};
}
async function setCoinSuspended(
ws: InternalWalletState,
coinPub: string,
suspended: boolean,
): Promise<void> {
await ws.db
.mktx((x) => [x.coins, x.coinAvailability])
.runReadWrite(async (tx) => {
const c = await tx.coins.get(coinPub);
if (!c) {
logger.warn(`coin ${coinPub} not found, won't suspend`);
return;
}
const coinAvailability = await tx.coinAvailability.get([
c.exchangeBaseUrl,
c.denomPubHash,
c.maxAge,
]);
checkDbInvariant(!!coinAvailability);
if (suspended) {
if (c.status !== CoinStatus.Fresh) {
return;
}
if (coinAvailability.freshCoinCount === 0) {
throw Error(
`invalid coin count ${coinAvailability.freshCoinCount} in DB`,
);
}
coinAvailability.freshCoinCount--;
c.status = CoinStatus.FreshSuspended;
} else {
if (c.status == CoinStatus.Dormant) {
return;
}
coinAvailability.freshCoinCount++;
c.status = CoinStatus.Fresh;
}
await tx.coins.put(c);
await tx.coinAvailability.put(coinAvailability);
});
}
2019-11-30 00:36:20 +01:00
/**
* Dump the public information of coins we have in an easy-to-process format.
*/
async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
const coinsJson: CoinDumpJson = { coins: [] };
2022-01-13 22:01:14 +01:00
logger.info("dumping coins");
await ws.db
.mktx((x) => [x.coins, x.denominations, x.withdrawalGroups])
.runReadOnly(async (tx) => {
const coins = await tx.coins.iter().toArray();
for (const c of coins) {
const denom = await tx.denominations.get([
c.exchangeBaseUrl,
c.denomPubHash,
]);
if (!denom) {
console.error("no denom session found for coin");
continue;
}
const cs = c.coinSource;
let refreshParentCoinPub: string | undefined;
if (cs.type == CoinSourceType.Refresh) {
refreshParentCoinPub = cs.oldCoinPub;
}
let withdrawalReservePub: string | undefined;
if (cs.type == CoinSourceType.Withdraw) {
const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId);
if (!ws) {
console.error("no withdrawal session found for coin");
continue;
}
withdrawalReservePub = ws.reservePub;
}
const denomInfo = await ws.getDenomInfo(
ws,
tx,
c.exchangeBaseUrl,
c.denomPubHash,
);
if (!denomInfo) {
console.error("no denomination found for coin");
continue;
}
coinsJson.coins.push({
coin_pub: c.coinPub,
denom_pub: denomInfo.denomPub,
denom_pub_hash: c.denomPubHash,
denom_value: Amounts.stringify({
value: denom.amountVal,
currency: denom.currency,
fraction: denom.amountFrac,
}),
exchange_base_url: c.exchangeBaseUrl,
refresh_parent_coin_pub: refreshParentCoinPub,
remaining_value: Amounts.stringify(c.currentAmount),
withdrawal_reserve_pub: withdrawalReservePub,
coin_suspended: c.status === CoinStatus.FreshSuspended,
ageCommitmentProof: c.ageCommitmentProof,
});
}
});
return coinsJson;
}
/**
* Get an API client from an internal wallet state object.
*/
export async function getClientFromWalletState(
ws: InternalWalletState,
): Promise<WalletCoreApiClient> {
let id = 0;
const client: WalletCoreApiClient = {
async call(op, payload): Promise<any> {
const res = await handleCoreApiRequest(ws, op, `${id++}`, payload);
switch (res.type) {
case "error":
throw TalerError.fromUncheckedDetail(res.error);
case "response":
return res.result;
}
},
};
return client;
}
declare const __VERSION__: string;
declare const __GIT_HASH__: string;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
2022-09-05 18:12:30 +02:00
/**
* Implementation of the "wallet-core" API.
*/
async function dispatchRequestInternal(
ws: InternalWalletState,
operation: string,
payload: unknown,
): Promise<Record<string, any>> {
2021-07-30 23:35:38 +02:00
if (!ws.initCalled && operation !== "initWallet") {
throw Error(
`wallet must be initialized before running operation ${operation}`,
);
}
// FIXME: Can we make this more type-safe by using the request/response type
// definitions we already have?
switch (operation) {
case "initWallet": {
2022-10-07 12:43:17 +02:00
logger.trace("initializing wallet");
ws.initCalled = true;
if (typeof payload === "object" && (payload as any).skipDefaults) {
2022-10-07 12:43:17 +02:00
logger.trace("skipping defaults");
} else {
2022-10-07 12:43:17 +02:00
logger.trace("filling defaults");
await fillDefaults(ws);
}
await maybeInitDevMode(ws);
return {};
}
case "withdrawTestkudos": {
await withdrawTestBalance(ws, {
amount: "TESTKUDOS:10",
bankBaseUrl: "https://bank.test.taler.net/",
bankAccessApiBaseUrl: "https://bank.test.taler.net/",
exchangeBaseUrl: "https://exchange.test.taler.net/",
});
return {};
}
case "withdrawTestBalance": {
const req = codecForWithdrawTestBalance().decode(payload);
await withdrawTestBalance(ws, req);
return {};
}
case "runIntegrationTest": {
const req = codecForIntegrationTestArgs().decode(payload);
await runIntegrationTest(ws, req);
return {};
}
case "testPay": {
const req = codecForTestPayArgs().decode(payload);
return await testPay(ws, req);
}
case "getTransactions": {
const req = codecForTransactionsRequest().decode(payload);
return await getTransactions(ws, req);
}
case "getTransactionById": {
const req = codecForTransactionByIdRequest().decode(payload);
2022-09-16 17:21:54 +02:00
return await getTransactionById(ws, req);
}
case "addExchange": {
const req = codecForAddExchangeRequest().decode(payload);
await updateExchangeFromUrl(ws, req.exchangeBaseUrl, {
forceNow: req.forceUpdate,
});
return {};
}
case "listExchanges": {
return await getExchanges(ws);
}
case "getExchangeDetailedInfo": {
const req = codecForAddExchangeRequest().decode(payload);
return await getExchangeDetailedInfo(ws, req.exchangeBaseUrl);
}
2021-12-23 19:17:36 +01:00
case "listKnownBankAccounts": {
const req = codecForListKnownBankAccounts().decode(payload);
return await listKnownBankAccounts(ws, req.currency);
}
2022-09-23 20:17:29 +02:00
case "addKnownBankAccounts": {
const req = codecForAddKnownBankAccounts().decode(payload);
await addKnownBankAccounts(ws, req.payto, req.alias, req.currency);
return {};
}
case "forgetKnownBankAccounts": {
const req = codecForForgetKnownBankAccounts().decode(payload);
await forgetKnownBankAccounts(ws, req.payto);
return {};
}
case "getWithdrawalDetailsForUri": {
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri);
}
case "getExchangeWithdrawalInfo": {
const req = codecForGetExchangeWithdrawalInfo().decode(payload);
2021-10-14 11:36:43 +02:00
return await getExchangeWithdrawalInfo(
ws,
req.exchangeBaseUrl,
req.amount,
req.ageRestricted,
2021-10-14 11:36:43 +02:00
);
}
case "acceptManualWithdrawal": {
const req = codecForAcceptManualWithdrawalRequet().decode(payload);
2022-08-09 15:00:45 +02:00
const res = await createManualWithdrawal(ws, {
amount: Amounts.parseOrThrow(req.amount),
exchangeBaseUrl: req.exchangeBaseUrl,
restrictAge: req.restrictAge,
});
return res;
}
case "getWithdrawalDetailsForAmount": {
2022-01-13 12:08:31 +01:00
const req =
codecForGetWithdrawalDetailsForAmountRequest().decode(payload);
return await getWithdrawalDetailsForAmount(
ws,
req.exchangeBaseUrl,
Amounts.parseOrThrow(req.amount),
2022-09-05 18:12:30 +02:00
req.restrictAge,
);
}
case "getBalances": {
return await getBalances(ws);
}
case "getPendingOperations": {
return await getPendingOperations(ws);
}
case "setExchangeTosAccepted": {
const req = codecForAcceptExchangeTosRequest().decode(payload);
await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl, req.etag);
return {};
}
case "applyRefund": {
const req = codecForApplyRefundRequest().decode(payload);
return await applyRefund(ws, req.talerRefundUri);
}
case "applyRefundFromPurchaseId": {
const req = codecForApplyRefundFromPurchaseIdRequest().decode(payload);
return await applyRefundFromPurchaseId(ws, req.purchaseId);
}
case "acceptBankIntegratedWithdrawal": {
2022-01-13 12:08:31 +01:00
const req =
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
2022-08-09 15:00:45 +02:00
return await acceptWithdrawalFromUri(ws, {
selectedExchange: req.exchangeBaseUrl,
talerWithdrawUri: req.talerWithdrawUri,
forcedDenomSel: req.forcedDenomSel,
restrictAge: req.restrictAge,
});
}
case "getExchangeTos": {
const req = codecForGetExchangeTosRequest().decode(payload);
2021-10-14 11:36:43 +02:00
return getExchangeTos(ws, req.exchangeBaseUrl, req.acceptedFormat);
}
case "getContractTermsDetails": {
const req = codecForGetContractTermsDetails().decode(payload);
return getContractTermsDetails(ws, req.proposalId);
}
case "retryPendingNow": {
await runPending(ws, true);
return {};
}
// FIXME: Deprecate one of the aliases!
case "preparePayForUri":
case "preparePay": {
const req = codecForPreparePayRequest().decode(payload);
return await preparePayForUri(ws, req.talerPayUri);
}
case "confirmPay": {
const req = codecForConfirmPayRequest().decode(payload);
return await confirmPay(ws, req.proposalId, req.sessionId);
}
case "abortFailedPayWithRefund": {
const req = codecForAbortPayWithRefundRequest().decode(payload);
await abortFailedPayWithRefund(ws, req.proposalId);
return {};
}
case "dumpCoins": {
return await dumpCoins(ws);
}
case "setCoinSuspended": {
const req = codecForSetCoinSuspendedRequest().decode(payload);
await setCoinSuspended(ws, req.coinPub, req.suspended);
return {};
}
case "forceRefresh": {
const req = codecForForceRefreshRequest().decode(payload);
const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
const refreshGroupId = await ws.db
.mktx((x) => [
x.refreshGroups,
x.coinAvailability,
x.denominations,
x.coins,
])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
2019-12-16 12:53:22 +01:00
return await createRefreshGroup(
ws,
2019-12-16 12:53:22 +01:00
tx,
coinPubs,
2019-12-16 12:53:22 +01:00
RefreshReason.Manual,
);
2021-06-09 15:14:17 +02:00
});
processRefreshGroup(ws, refreshGroupId.refreshGroupId, {
forceNow: true,
}).catch((x) => {
logger.error(x);
});
return {
refreshGroupId,
};
}
case "prepareTip": {
const req = codecForPrepareTipRequest().decode(payload);
return await prepareTip(ws, req.talerTipUri);
}
case "prepareRefund": {
const req = codecForPrepareRefundRequest().decode(payload);
return await prepareRefund(ws, req.talerRefundUri);
}
case "acceptTip": {
const req = codecForAcceptTipRequest().decode(payload);
return await acceptTip(ws, req.walletTipId);
2019-11-30 00:36:20 +01:00
}
case "exportBackupPlain": {
return exportBackup(ws);
}
case "addBackupProvider": {
const req = codecForAddBackupProviderRequest().decode(payload);
await addBackupProvider(ws, req);
return {};
}
case "runBackupCycle": {
2021-07-07 18:28:02 +02:00
const req = codecForRunBackupCycle().decode(payload);
await runBackupCycle(ws, req);
return {};
}
case "removeBackupProvider": {
const req = codecForRemoveBackupProvider().decode(payload);
await removeBackupProvider(ws, req);
return {};
}
case "exportBackupRecovery": {
const resp = await getBackupRecovery(ws);
return resp;
}
case "importBackupRecovery": {
const req = codecForAny().decode(payload);
await loadBackupRecovery(ws, req);
return {};
}
case "getBackupInfo": {
const resp = await getBackupInfo(ws);
return resp;
}
2021-12-23 19:17:36 +01:00
case "getFeeForDeposit": {
const req = codecForGetFeeForDeposit().decode(payload);
return await getFeeForDeposit(ws, req);
}
2022-05-03 05:16:03 +02:00
case "prepareDeposit": {
const req = codecForPrepareDepositRequest().decode(payload);
return await prepareDepositGroup(ws, req);
}
case "createDepositGroup": {
const req = codecForCreateDepositGroupRequest().decode(payload);
return await createDepositGroup(ws, req);
}
case "trackDepositGroup": {
const req = codecForTrackDepositGroupRequest().decode(payload);
return trackDepositGroup(ws, req);
}
case "deleteTransaction": {
const req = codecForDeleteTransactionRequest().decode(payload);
await deleteTransaction(ws, req.transactionId);
return {};
}
case "retryTransaction": {
const req = codecForRetryTransactionRequest().decode(payload);
await retryTransaction(ws, req.transactionId);
return {};
}
case "setWalletDeviceId": {
const req = codecForSetWalletDeviceIdRequest().decode(payload);
await setWalletDeviceId(ws, req.walletDeviceId);
return {};
}
case "listCurrencies": {
return await ws.db
.mktx((x) => [x.auditorTrust, x.exchangeTrust])
.runReadOnly(async (tx) => {
const trustedAuditors = await tx.auditorTrust.iter().toArray();
const trustedExchanges = await tx.exchangeTrust.iter().toArray();
return {
trustedAuditors: trustedAuditors.map((x) => ({
currency: x.currency,
auditorBaseUrl: x.auditorBaseUrl,
auditorPub: x.auditorPub,
})),
trustedExchanges: trustedExchanges.map((x) => ({
currency: x.currency,
exchangeBaseUrl: x.exchangeBaseUrl,
exchangeMasterPub: x.exchangeMasterPub,
})),
};
});
}
2021-10-14 11:36:43 +02:00
case "withdrawFakebank": {
const req = codecForWithdrawFakebankRequest().decode(payload);
const amount = Amounts.parseOrThrow(req.amount);
const details = await getWithdrawalDetailsForAmount(
ws,
req.exchange,
amount,
2022-09-05 18:12:30 +02:00
undefined,
2021-10-14 11:36:43 +02:00
);
2022-08-09 15:00:45 +02:00
const wres = await createManualWithdrawal(ws, {
amount: amount,
exchangeBaseUrl: req.exchange,
});
2021-10-14 11:36:43 +02:00
const paytoUri = details.paytoUris[0];
const pt = parsePaytoUri(paytoUri);
if (!pt) {
throw Error("failed to parse payto URI");
}
const components = pt.targetPath.split("/");
const creditorAcct = components[components.length - 1];
logger.info(`making testbank transfer to '${creditorAcct}'`);
2021-10-14 11:36:43 +02:00
const fbReq = await ws.http.postJson(
new URL(`${creditorAcct}/admin/add-incoming`, req.bank).href,
{
amount: Amounts.stringify(amount),
reserve_pub: wres.reservePub,
debit_account: "payto://x-taler-bank/localhost/testdebtor",
},
);
const fbResp = await readSuccessResponseJsonOrThrow(fbReq, codecForAny());
logger.info(`started fakebank withdrawal: ${j2s(fbResp)}`);
return {};
}
case "testCrypto": {
return await ws.cryptoApi.hashString({ str: "hello world" });
}
case "clearDb":
await clearDatabase(ws.db.idbHandle());
return {};
case "recycle": {
const backup = await exportBackup(ws);
await clearDatabase(ws.db.idbHandle());
await importBackupPlain(ws, backup);
return {};
}
case "exportDb": {
const dbDump = await exportDb(ws.db.idbHandle());
return dbDump;
}
case "importDb": {
const req = codecForImportDbRequest().decode(payload);
await importDb(ws.db.idbHandle(), req.dump);
return [];
}
case "initiatePeerPushPayment": {
const req = codecForInitiatePeerPushPaymentRequest().decode(payload);
return await initiatePeerToPeerPush(ws, req);
}
case "checkPeerPushPayment": {
const req = codecForCheckPeerPushPaymentRequest().decode(payload);
return await checkPeerPushPayment(ws, req);
}
case "acceptPeerPushPayment": {
const req = codecForAcceptPeerPushPaymentRequest().decode(payload);
2022-09-17 23:40:03 +02:00
return await acceptPeerPushPayment(ws, req);
}
case "initiatePeerPullPayment": {
const req = codecForInitiatePeerPullPaymentRequest().decode(payload);
return await initiatePeerRequestForPay(ws, req);
}
case "checkPeerPullPayment": {
const req = codecForCheckPeerPullPaymentRequest().decode(payload);
return await checkPeerPullPayment(ws, req);
}
case "acceptPeerPullPayment": {
const req = codecForAcceptPeerPullPaymentRequest().decode(payload);
2022-09-17 23:40:03 +02:00
return await acceptPeerPullPayment(ws, req);
}
case "applyDevExperiment": {
const req = codecForApplyDevExperiment().decode(payload);
await applyDevExperiment(ws, req.devExperimentUri);
return {};
}
case "setDevMode": {
const req = codecForSetDevModeRequest().decode(payload);
await setDevMode(ws, req.devModeEnabled);
return {};
}
case "getVersion": {
const version: WalletCoreVersion = {
hash: GIT_HASH,
version: VERSION,
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
devMode: ws.devModeActive,
};
return version;
}
}
throw TalerError.fromDetail(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
{
operation,
},
"unknown operation",
);
}
/**
* Handle a request to the wallet-core API.
*/
export async function handleCoreApiRequest(
ws: InternalWalletState,
operation: string,
id: string,
payload: unknown,
): Promise<CoreApiResponse> {
try {
const result = await dispatchRequestInternal(ws, operation, payload);
return {
type: "response",
operation,
id,
result,
};
2021-10-14 11:36:43 +02:00
} catch (e: any) {
const err = getErrorDetailFromException(e);
logger.info(`finished wallet core request with error: ${j2s(err)}`);
return {
type: "error",
operation,
id,
error: err,
};
}
2016-10-18 01:16:31 +02:00
}
/**
* Public handle to a running wallet.
*/
export class Wallet {
private ws: InternalWalletState;
2022-09-16 17:41:13 +02:00
private _client: WalletCoreApiClient | undefined;
private constructor(
db: DbAccess<typeof WalletStoresV1>,
http: HttpRequestLibrary,
timer: TimerAPI,
cryptoWorkerFactory: CryptoWorkerFactory,
) {
this.ws = new InternalWalletStateImpl(db, http, timer, cryptoWorkerFactory);
}
get client(): WalletCoreApiClient {
2022-09-16 17:41:13 +02:00
if (!this._client) {
throw Error();
}
return this._client;
}
/**
* Trust the exchange, do not validate signatures.
* Only used to benchmark the exchange.
*/
setInsecureTrustExchange(): void {
this.ws.insecureTrustExchange = true;
}
setBatchWithdrawal(enable: boolean): void {
this.ws.batchWithdrawal = enable;
}
static async create(
db: DbAccess<typeof WalletStoresV1>,
http: HttpRequestLibrary,
timer: TimerAPI,
cryptoWorkerFactory: CryptoWorkerFactory,
): Promise<Wallet> {
const w = new Wallet(db, http, timer, cryptoWorkerFactory);
w._client = await getClientFromWalletState(w.ws);
return w;
}
addNotificationListener(f: (n: WalletNotification) => void): void {
return this.ws.addNotificationListener(f);
}
stop(): void {
this.ws.stop();
}
runPending(forceNow = false): Promise<void> {
return runPending(this.ws, forceNow);
}
runTaskLoop(opts?: RetryLoopOpts): Promise<TaskLoopResult> {
return runTaskLoop(this.ws, opts);
}
handleCoreApiRequest(
operation: string,
id: string,
payload: unknown,
): Promise<CoreApiResponse> {
return handleCoreApiRequest(this.ws, operation, id, payload);
}
}
/**
* Internal state of the wallet.
*
* This ties together all the operation implementations.
*/
class InternalWalletStateImpl implements InternalWalletState {
/**
* @see {@link InternalWalletState.activeLongpoll}
*/
activeLongpoll: ActiveLongpollInfo = {};
2022-03-23 21:24:23 +01:00
cryptoApi: TalerCryptoInterface;
cryptoDispatcher: CryptoDispatcher;
merchantInfoCache: Record<string, MerchantInfo> = {};
insecureTrustExchange = false;
batchWithdrawal = false;
readonly timerGroup: TimerGroup;
latch = new AsyncCondition();
stopped = false;
listeners: NotificationListener[] = [];
initCalled = false;
devModeActive = false;
exchangeOps: ExchangeOperations = {
getExchangeDetails,
getExchangeTrust,
updateExchangeFromUrl,
};
2021-06-22 12:18:12 +02:00
recoupOps: RecoupOperations = {
createRecoupGroup,
processRecoupGroup,
2021-06-22 12:18:12 +02:00
};
merchantOps: MerchantOperations = {
getMerchantInfo,
};
refreshOps: RefreshOperations = {
createRefreshGroup,
};
2022-01-13 12:08:31 +01:00
// FIXME: Use an LRU cache here.
private denomCache: Record<string, DenominationInfo> = {};
/**
* Promises that are waiting for a particular resource.
*/
private resourceWaiters: Record<string, OpenedPromise<void>[]> = {};
/**
* Resources that are currently locked.
*/
private resourceLocks: Set<string> = new Set();
constructor(
// FIXME: Make this a getter and make
// the actual value nullable.
// Check if we are in a DB migration / garbage collection
// and throw an error in that case.
public db: DbAccess<typeof WalletStoresV1>,
public http: HttpRequestLibrary,
public timer: TimerAPI,
cryptoWorkerFactory: CryptoWorkerFactory,
) {
2022-03-23 21:24:23 +01:00
this.cryptoDispatcher = new CryptoDispatcher(cryptoWorkerFactory);
this.cryptoApi = this.cryptoDispatcher.cryptoApi;
this.timerGroup = new TimerGroup(timer);
}
2022-01-13 12:08:31 +01:00
async getDenomInfo(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
denominations: typeof WalletStoresV1.denominations;
}>,
exchangeBaseUrl: string,
denomPubHash: string,
): Promise<DenominationInfo | undefined> {
2022-01-13 12:08:31 +01:00
const key = `${exchangeBaseUrl}:${denomPubHash}`;
const cached = this.denomCache[key];
if (cached) {
return cached;
}
2022-01-13 22:01:14 +01:00
const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
if (d) {
return DenominationRecord.toDenomInfo(d);
2022-01-13 22:01:14 +01:00
}
return undefined;
2022-01-13 12:08:31 +01:00
}
notify(n: WalletNotification): void {
logger.trace("Notification", n);
for (const l of this.listeners) {
const nc = JSON.parse(JSON.stringify(n));
setTimeout(() => {
l(nc);
}, 0);
}
}
addNotificationListener(f: (n: WalletNotification) => void): void {
this.listeners.push(f);
}
/**
* Stop ongoing processing.
*/
stop(): void {
logger.trace("stopping (at internal wallet state)");
this.stopped = true;
this.timerGroup.stopCurrentAndFutureTimers();
2022-03-23 21:24:23 +01:00
this.cryptoDispatcher.stop();
for (const key of Object.keys(this.activeLongpoll)) {
logger.trace(`cancelling active longpoll ${key}`);
this.activeLongpoll[key].cancel();
}
}
2021-06-22 12:18:12 +02:00
async runUntilDone(
req: {
maxRetries?: number;
} = {},
): Promise<void> {
await runTaskLoop(this, { ...req, stopWhenDone: true });
2021-06-22 12:18:12 +02:00
}
/**
* Run an async function after acquiring a list of locks, identified
* by string tokens.
*/
async runSequentialized<T>(
tokens: string[],
f: () => Promise<T>,
): Promise<T> {
// Make sure locks are always acquired in the same order
tokens = [...tokens].sort();
for (const token of tokens) {
if (this.resourceLocks.has(token)) {
const p = openPromise<void>();
let waitList = this.resourceWaiters[token];
if (!waitList) {
waitList = this.resourceWaiters[token] = [];
}
waitList.push(p);
await p.promise;
}
this.resourceLocks.add(token);
}
try {
logger.trace(`begin exclusive execution on ${JSON.stringify(tokens)}`);
const result = await f();
logger.trace(`end exclusive execution on ${JSON.stringify(tokens)}`);
return result;
} finally {
for (const token of tokens) {
this.resourceLocks.delete(token);
let waiter = (this.resourceWaiters[token] ?? []).shift();
if (waiter) {
waiter.resolve();
}
}
}
}
}