aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-01-08 21:17:00 +0100
committerFlorian Dold <florian@dold.me>2024-01-08 21:17:08 +0100
commit6f2b03021d7946a61d6b8e53dbba7fc10e5f9a4d (patch)
tree82a3985ab7267e6f4a57c0b275f1929558b5e572
parentc019f4c040e82baebdbbda8208f10be2fbc19566 (diff)
wallet-core: exchange management cleanup
-rw-r--r--packages/taler-wallet-core/src/internal-wallet-state.ts24
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts7
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts17
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts130
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts20
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts63
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts91
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts7
-rw-r--r--packages/taler-wallet-core/src/util/instructedAmountConversion.ts18
-rw-r--r--packages/taler-wallet-core/src/wallet.ts39
10 files changed, 242 insertions, 174 deletions
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts
index b1389a359..94f2367e1 100644
--- a/packages/taler-wallet-core/src/internal-wallet-state.ts
+++ b/packages/taler-wallet-core/src/internal-wallet-state.ts
@@ -95,29 +95,6 @@ export interface RefreshOperations {
): Promise<RefreshGroupId>;
}
-/**
- * Interface for exchange-related operations.
- */
-export interface ExchangeOperations {
- // FIXME: Should other operations maybe always use
- // updateExchangeFromUrl?
- getExchangeDetails(
- tx: GetReadOnlyAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- }>,
- exchangeBaseUrl: string,
- ): Promise<ExchangeDetailsRecord | undefined>;
- fetchFreshExchange(
- ws: InternalWalletState,
- baseUrl: string,
- options?: {
- forceNow?: boolean;
- cancellationToken?: CancellationToken;
- },
- ): Promise<ReadyExchangeSummary>;
-}
-
export interface RecoupOperations {
createRecoupGroup(
ws: InternalWalletState,
@@ -176,7 +153,6 @@ export interface InternalWalletState {
merchantInfoCache: Record<string, MerchantInfo>;
- exchangeOps: ExchangeOperations;
recoupOps: RecoupOperations;
merchantOps: MerchantOperations;
refreshOps: RefreshOperations;
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
index fdaab0d5f..53ca33fe7 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -49,6 +49,7 @@
/**
* Imports.
*/
+import { GlobalIDB } from "@gnu-taler/idb-bridge";
import {
AllowedAuditorInfo,
AllowedExchangeInfo,
@@ -67,7 +68,6 @@ import {
OPERATION_STATUS_ACTIVE_FIRST,
OPERATION_STATUS_ACTIVE_LAST,
RefreshGroupRecord,
- RefreshOperationStatus,
WalletStoresV1,
WithdrawalGroupStatus,
} from "../db.js";
@@ -75,8 +75,7 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess } from "../util/query.js";
-import { getExchangeDetails } from "./exchanges.js";
-import { GlobalIDB } from "@gnu-taler/idb-bridge";
+import { getExchangeWireDetailsInTx } from "./exchanges.js";
/**
* Logger.
@@ -516,7 +515,7 @@ export async function getBalanceDetail(
.runReadOnly(async (tx) => {
const allExchanges = await tx.exchanges.iter().toArray();
for (const e of allExchanges) {
- const details = await getExchangeDetails(tx, e.baseUrl);
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
if (!details || req.currency !== details.currency) {
continue;
}
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 8205b7583..f158d9cf9 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -75,7 +75,6 @@ import {
getCandidateWithdrawalDenomsTx,
getTotalRefreshCost,
timestampPreciseToDb,
- timestampProtocolFromDb,
timestampProtocolToDb,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
@@ -89,7 +88,7 @@ import {
runLongpollAsync,
spendCoins,
} from "./common.js";
-import { getExchangeDetails } from "./exchanges.js";
+import { getExchangeWireDetailsInTx } from "./exchanges.js";
import {
extractContractData,
generateDepositPermissions,
@@ -1168,7 +1167,7 @@ export async function prepareDepositGroup(
.runReadOnly(async (tx) => {
const allExchanges = await tx.exchanges.iter().toArray();
for (const e of allExchanges) {
- const details = await getExchangeDetails(tx, e.baseUrl);
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
if (!details || amount.currency !== details.currency) {
continue;
}
@@ -1282,7 +1281,7 @@ export async function createDepositGroup(
.runReadOnly(async (tx) => {
const allExchanges = await tx.exchanges.iter().toArray();
for (const e of allExchanges) {
- const details = await getExchangeDetails(tx, e.baseUrl);
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
if (!details || amount.currency !== details.currency) {
continue;
}
@@ -1495,7 +1494,10 @@ export async function getCounterpartyEffectiveDepositAmount(
}
for (const exchangeUrl of exchangeSet.values()) {
- const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ exchangeUrl,
+ );
if (!exchangeDetails) {
continue;
}
@@ -1574,7 +1576,10 @@ async function getTotalFeesForDepositAmount(
}
for (const exchangeUrl of exchangeSet.values()) {
- const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ exchangeUrl,
+ );
if (!exchangeDetails) {
continue;
}
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 67d598e70..766af27a8 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -32,6 +32,7 @@ import {
DenominationInfo,
DenominationPubKey,
Duration,
+ EddsaPublicKeyString,
ExchangeAuditor,
ExchangeDetailedResponse,
ExchangeGlobalFees,
@@ -41,6 +42,7 @@ import {
ExchangeWireAccount,
ExchangesListResponse,
FeeDescription,
+ GetExchangeEntryByUrlRequest,
GetExchangeTosResult,
GlobalFees,
LibtoolVersion,
@@ -175,10 +177,8 @@ async function downloadExchangeWithTermsOfService(
/**
* Get exchange details from the database.
- *
- * FIXME: Should we encapsulate the result better, instead of returning the raw DB records here?
*/
-export async function getExchangeDetails(
+async function getExchangeRecordsInternal(
tx: GetReadOnlyAccess<{
exchanges: typeof WalletStoresV1.exchanges;
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
@@ -201,6 +201,67 @@ export async function getExchangeDetails(
]);
}
+export interface ExchangeWireDetails {
+ currency: string;
+ masterPublicKey: EddsaPublicKeyString;
+ wireInfo: WireInfo;
+ exchangeBaseUrl: string;
+ auditors: ExchangeAuditor[];
+ globalFees: ExchangeGlobalFees[];
+}
+
+export async function getExchangeWireDetailsInTx(
+ tx: GetReadOnlyAccess<{
+ exchanges: typeof WalletStoresV1.exchanges;
+ exchangeDetails: typeof WalletStoresV1.exchangeDetails;
+ }>,
+ exchangeBaseUrl: string,
+): Promise<ExchangeWireDetails | undefined> {
+ const det = await getExchangeRecordsInternal(tx, exchangeBaseUrl);
+ if (!det) {
+ return undefined;
+ }
+ return {
+ currency: det.currency,
+ masterPublicKey: det.masterPublicKey,
+ wireInfo: det.wireInfo,
+ exchangeBaseUrl: det.exchangeBaseUrl,
+ auditors: det.auditors,
+ globalFees: det.globalFees,
+ };
+}
+
+export async function lookupExchangeByUri(
+ ws: InternalWalletState,
+ req: GetExchangeEntryByUrlRequest,
+): Promise<ExchangeListItem> {
+ return await ws.db
+ .mktx((x) => [
+ x.exchanges,
+ x.exchangeDetails,
+ x.denominations,
+ x.operationRetries,
+ ])
+ .runReadOnly(async (tx) => {
+ const exchangeRec = await tx.exchanges.get(req.exchangeBaseUrl);
+ if (!exchangeRec) {
+ throw Error("exchange not found");
+ }
+ const exchangeDetails = await getExchangeRecordsInternal(
+ tx,
+ exchangeRec.baseUrl,
+ );
+ const opRetryRecord = await tx.operationRetries.get(
+ TaskIdentifiers.forExchangeUpdate(exchangeRec),
+ );
+ return makeExchangeListItem(
+ exchangeRec,
+ exchangeDetails,
+ opRetryRecord?.lastError,
+ );
+ });
+}
+
/**
* Mark a ToS version as accepted by the user.
*
@@ -417,7 +478,7 @@ async function provideExchangeRecordInTx(
newExchangeState: getExchangeState(r),
};
}
- const exchangeDetails = await getExchangeDetails(tx, baseUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(tx, baseUrl);
return { exchange, exchangeDetails, notification };
}
@@ -825,7 +886,7 @@ export async function fetchFreshExchange(
.mktx((x) => [x.exchanges, x.exchangeDetails, x.operationRetries])
.runReadOnly(async (tx) => {
const exchange = await tx.exchanges.get(canonUrl);
- const exchangeDetails = await getExchangeDetails(tx, canonUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(tx, canonUrl);
const retryInfo = await tx.operationRetries.get(operationId);
return { exchange, exchangeDetails, retryInfo };
});
@@ -980,7 +1041,7 @@ export async function updateExchangeFromUrlHandler(
return;
}
const oldExchangeState = getExchangeState(r);
- const existingDetails = await getExchangeDetails(tx, r.baseUrl);
+ const existingDetails = await getExchangeRecordsInternal(tx, r.baseUrl);
if (!existingDetails) {
detailsPointerChanged = true;
}
@@ -1173,7 +1234,7 @@ export async function getExchangePaytoUri(
const details = await ws.db
.mktx((x) => [x.exchangeDetails, x.exchanges])
.runReadOnly(async (tx) => {
- return getExchangeDetails(tx, exchangeBaseUrl);
+ return getExchangeRecordsInternal(tx, exchangeBaseUrl);
});
const accounts = details?.wireInfo.accounts ?? [];
for (const account of accounts) {
@@ -1202,7 +1263,6 @@ export async function getExchangeTos(
acceptedFormat?: string[],
acceptLanguage?: string,
): Promise<GetExchangeTosResult> {
- // FIXME: download ToS in acceptable format if passed!
const exch = await fetchFreshExchange(ws, exchangeBaseUrl);
const tosDownload = await downloadTosFromAcceptedFormat(
@@ -1234,6 +1294,10 @@ export async function getExchangeTos(
};
}
+/**
+ * Parsed information about an exchange,
+ * obtained by requesting /keys.
+ */
export interface ExchangeInfo {
keys: ExchangeKeysDownloadResult;
}
@@ -1273,7 +1337,7 @@ export async function listExchanges(
tag: PendingTaskType.ExchangeUpdate,
exchangeBaseUrl: r.baseUrl,
});
- const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(tx, r.baseUrl);
const opRetryRecord = await tx.operationRetries.get(taskId);
exchanges.push(
makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError),
@@ -1283,11 +1347,55 @@ export async function listExchanges(
return { exchanges };
}
+/**
+ * Transition an exchange to the "used" entry state if necessary.
+ *
+ * Should be called whenever the exchange is actively used by the client (for withdrawals etc.).
+ */
+export async function markExchangeUsed(
+ ws: InternalWalletState,
+ tx: GetReadWriteAccess<{ exchanges: typeof WalletStoresV1.exchanges }>,
+ exchangeBaseUrl: string,
+): Promise<{ notif: WalletNotification | undefined }> {
+ exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+ logger.info(`marking exchange ${exchangeBaseUrl} as used`);
+ const exch = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exch) {
+ return {
+ notif: undefined,
+ };
+ }
+ const oldExchangeState = getExchangeState(exch);
+ switch (exch.entryStatus) {
+ case ExchangeEntryDbRecordStatus.Ephemeral:
+ case ExchangeEntryDbRecordStatus.Preset: {
+ exch.entryStatus = ExchangeEntryDbRecordStatus.Used;
+ await tx.exchanges.put(exch);
+ const newExchangeState = getExchangeState(exch);
+ return {
+ notif: {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl,
+ newExchangeState: newExchangeState,
+ oldExchangeState: oldExchangeState,
+ } satisfies WalletNotification,
+ };
+ }
+ default:
+ return {
+ notif: undefined,
+ };
+ }
+}
+
+/**
+ * Get detailed information about the exchange including a timeline
+ * for the fees charged by the exchange.
+ */
export async function getExchangeDetailedInfo(
ws: InternalWalletState,
exchangeBaseurl: string,
): Promise<ExchangeDetailedResponse> {
- // TODO: should we use the forceUpdate parameter?
const exchange = await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
.runReadOnly(async (tx) => {
@@ -1297,7 +1405,7 @@ export async function getExchangeDetailedInfo(
return;
}
const { currency } = dp;
- const exchangeDetails = await getExchangeDetails(tx, ex.baseUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(tx, ex.baseUrl);
if (!exchangeDetails) {
return;
}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
index 78263c4c3..6b7b62393 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -69,7 +69,7 @@ import {
constructTaskIdentifier,
runLongpollAsync,
} from "./common.js";
-import { fetchFreshExchange } from "./exchanges.js";
+import { fetchFreshExchange, markExchangeUsed } from "./exchanges.js";
import {
codecForExchangePurseStatus,
getMergeReserveInfo,
@@ -82,6 +82,7 @@ import {
stopLongpolling,
} from "./transactions.js";
import {
+ PerformCreateWithdrawalGroupResult,
getExchangeWithdrawalInfo,
internalPerformCreateWithdrawalGroup,
internalPrepareCreateWithdrawalGroup,
@@ -486,19 +487,20 @@ async function handlePendingMerge(
if (!peerInc) {
return undefined;
}
- let withdrawalTransition: TransitionInfo | undefined;
const oldTxState = computePeerPushCreditTransactionState(peerInc);
+ let wgCreateRes: PerformCreateWithdrawalGroupResult | undefined =
+ undefined;
switch (peerInc.status) {
case PeerPushCreditStatus.PendingMerge:
case PeerPushCreditStatus.PendingMergeKycRequired: {
peerInc.status = PeerPushCreditStatus.PendingWithdrawing;
- const wgRes = await internalPerformCreateWithdrawalGroup(
+ wgCreateRes = await internalPerformCreateWithdrawalGroup(
ws,
tx,
withdrawalGroupPrep,
);
- withdrawalTransition = wgRes.transitionInfo;
- peerInc.withdrawalGroupId = wgRes.withdrawalGroup.withdrawalGroupId;
+ peerInc.withdrawalGroupId =
+ wgCreateRes.withdrawalGroup.withdrawalGroupId;
break;
}
}
@@ -506,13 +508,17 @@ async function handlePendingMerge(
const newTxState = computePeerPushCreditTransactionState(peerInc);
return {
peerPushCreditTransition: { oldTxState, newTxState },
- withdrawalTransition,
+ wgCreateRes,
};
});
+ // Transaction was commited, now we can emit notifications.
+ if (txRes?.wgCreateRes?.exchangeNotif) {
+ ws.notify(txRes.wgCreateRes.exchangeNotif);
+ }
notifyTransition(
ws,
withdrawalGroupPrep.transactionId,
- txRes?.withdrawalTransition,
+ txRes?.wgCreateRes?.transitionInfo,
);
notifyTransition(ws, transactionId, txRes?.peerPushCreditTransition);
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 9deb050d8..3a219b39b 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -47,7 +47,6 @@ import {
import {
DepositElementStatus,
DepositGroupRecord,
- ExchangeDetailsRecord,
OperationRetryRecord,
PeerPullCreditRecord,
PeerPullDebitRecordStatus,
@@ -91,7 +90,10 @@ import {
resumeDepositGroup,
suspendDepositGroup,
} from "./deposits.js";
-import { getExchangeDetails } from "./exchanges.js";
+import {
+ ExchangeWireDetails,
+ getExchangeWireDetailsInTx,
+} from "./exchanges.js";
import {
abortPayMerchant,
computePayMerchantTransactionActions,
@@ -137,8 +139,8 @@ import {
} from "./pay-peer-push-debit.js";
import {
iterRecordsForDeposit,
- iterRecordsForPeerPullDebit,
iterRecordsForPeerPullInitiation as iterRecordsForPeerPullCredit,
+ iterRecordsForPeerPullDebit,
iterRecordsForPeerPushCredit,
iterRecordsForPeerPushInitiation as iterRecordsForPeerPushDebit,
iterRecordsForPurchase,
@@ -240,9 +242,8 @@ export async function getTransactionById(
x.operationRetries,
])
.runReadWrite(async (tx) => {
- const withdrawalGroupRecord = await tx.withdrawalGroups.get(
- withdrawalGroupId,
- );
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
if (!withdrawalGroupRecord) throw Error("not found");
@@ -258,7 +259,7 @@ export async function getTransactionById(
ort,
);
}
- const exchangeDetails = await getExchangeDetails(
+ const exchangeDetails = await getExchangeWireDetailsInTx(
tx,
withdrawalGroupRecord.exchangeBaseUrl,
);
@@ -290,7 +291,9 @@ export async function getTransactionById(
const payOpId = TaskIdentifiers.forPay(purchase);
const payRetryRecord = await tx.operationRetries.get(payOpId);
- const refunds = await tx.refundGroups.indexes.byProposalId.getAll(purchase.proposalId)
+ const refunds = await tx.refundGroups.indexes.byProposalId.getAll(
+ purchase.proposalId,
+ );
return buildTransactionForPurchase(
purchase,
@@ -544,7 +547,7 @@ function buildTransactionForPeerPullCredit(
const silentWithdrawalErrorForInvoice =
wsrOrt?.lastError &&
wsrOrt.lastError.code ===
- TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
+ TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => {
return (
e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
@@ -574,10 +577,10 @@ function buildTransactionForPeerPullCredit(
kycUrl: pullCredit.kycUrl,
...(wsrOrt?.lastError
? {
- error: silentWithdrawalErrorForInvoice
- ? undefined
- : wsrOrt.lastError,
- }
+ error: silentWithdrawalErrorForInvoice
+ ? undefined
+ : wsrOrt.lastError,
+ }
: {}),
};
}
@@ -698,7 +701,7 @@ function buildTransactionForBankIntegratedWithdraw(
function buildTransactionForManualWithdraw(
withdrawalGroup: WithdrawalGroupRecord,
- exchangeDetails: ExchangeDetailsRecord,
+ exchangeDetails: ExchangeWireDetails,
ort?: OperationRetryRecord,
): Transaction {
if (withdrawalGroup.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual)
@@ -725,7 +728,8 @@ function buildTransactionForManualWithdraw(
type: WithdrawalType.ManualTransfer,
reservePub: withdrawalGroup.reservePub,
exchangePaytoUris,
- exchangeCreditAccountDetails: withdrawalGroup.wgInfo.exchangeCreditAccounts,
+ exchangeCreditAccountDetails:
+ withdrawalGroup.wgInfo.exchangeCreditAccounts,
reserveIsReady:
withdrawalGroup.status === WithdrawalGroupStatus.Done ||
withdrawalGroup.status === WithdrawalGroupStatus.PendingReady,
@@ -928,14 +932,16 @@ async function buildTransactionForPurchase(
info.fulfillmentUrl = contractData.fulfillmentUrl;
}
- const refunds: RefundInfoShort[] = refundsInfo.map(r => ({
+ const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({
amountEffective: r.amountEffective,
amountRaw: r.amountRaw,
- timestamp: TalerPreciseTimestamp.round(timestampPreciseFromDb(r.timestampCreated)),
+ timestamp: TalerPreciseTimestamp.round(
+ timestampPreciseFromDb(r.timestampCreated),
+ ),
transactionId: constructTransactionIdentifier({
tag: TransactionType.Refund,
refundGroupId: r.refundGroupId,
- })
+ }),
}));
const timestamp = purchaseRecord.timestampAccept;
@@ -1193,7 +1199,7 @@ export async function getTransactions(
);
return;
case WithdrawalRecordType.BankManual: {
- const exchangeDetails = await getExchangeDetails(
+ const exchangeDetails = await getExchangeWireDetailsInTx(
tx,
wsr.exchangeBaseUrl,
);
@@ -1258,7 +1264,9 @@ export async function getTransactions(
const payOpId = TaskIdentifiers.forPay(purchase);
const payRetryRecord = await tx.operationRetries.get(payOpId);
- const refunds = await tx.refundGroups.indexes.byProposalId.getAll(purchase.proposalId)
+ const refunds = await tx.refundGroups.indexes.byProposalId.getAll(
+ purchase.proposalId,
+ );
transactions.push(
await buildTransactionForPurchase(
@@ -1723,9 +1731,8 @@ export async function deleteTransaction(
}
if (pushInc.withdrawalGroupId) {
const withdrawalGroupId = pushInc.withdrawalGroupId;
- const withdrawalGroupRecord = await tx.withdrawalGroups.get(
- withdrawalGroupId,
- );
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
if (withdrawalGroupRecord) {
await tx.withdrawalGroups.delete(withdrawalGroupId);
await tx.tombstones.put({
@@ -1753,9 +1760,8 @@ export async function deleteTransaction(
}
if (pullIni.withdrawalGroupId) {
const withdrawalGroupId = pullIni.withdrawalGroupId;
- const withdrawalGroupRecord = await tx.withdrawalGroups.get(
- withdrawalGroupId,
- );
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
if (withdrawalGroupRecord) {
await tx.withdrawalGroups.delete(withdrawalGroupId);
await tx.tombstones.put({
@@ -1778,9 +1784,8 @@ export async function deleteTransaction(
await ws.db
.mktx((x) => [x.withdrawalGroups, x.tombstones])
.runReadWrite(async (tx) => {
- const withdrawalGroupRecord = await tx.withdrawalGroups.get(
- withdrawalGroupId,
- );
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
if (withdrawalGroupRecord) {
await tx.withdrawalGroups.delete(withdrawalGroupId);
await tx.tombstones.put({
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index cf4e9a1d5..49c0e4a14 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -57,6 +57,7 @@ import {
TransactionType,
URL,
UnblindedSignature,
+ WalletNotification,
WithdrawUriInfoResponse,
WithdrawalExchangeAccountDetails,
addPaytoQueryParams,
@@ -133,8 +134,10 @@ import {
import {
ReadyExchangeSummary,
fetchFreshExchange,
- getExchangeDetails,
getExchangePaytoUri,
+ getExchangeWireDetailsInTx,
+ listExchanges,
+ markExchangeUsed,
} from "./exchanges.js";
import {
TransitionInfo,
@@ -1191,7 +1194,7 @@ export async function updateWithdrawalDenoms(
const exchangeDetails = await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails])
.runReadOnly(async (tx) => {
- return ws.exchangeOps.getExchangeDetails(tx, exchangeBaseUrl);
+ return getExchangeWireDetailsInTx(tx, exchangeBaseUrl);
});
if (!exchangeDetails) {
logger.error("exchange details not available");
@@ -1521,7 +1524,7 @@ async function processWithdrawalGroupPendingReady(
withdrawalGroupId,
});
- await ws.exchangeOps.fetchFreshExchange(ws, withdrawalGroup.exchangeBaseUrl);
+ await fetchFreshExchange(ws, withdrawalGroup.exchangeBaseUrl);
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
logger.warn("Finishing empty withdrawal group (no denoms)");
@@ -1768,7 +1771,7 @@ export async function getExchangeWithdrawalInfo(
ageRestricted: number | undefined,
): Promise<ExchangeWithdrawalDetails> {
logger.trace("updating exchange");
- const exchange = await ws.exchangeOps.fetchFreshExchange(ws, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(ws, exchangeBaseUrl);
if (exchange.currency != instructedAmount.currency) {
// Specifiying the amount in the conversion input currency is not yet supported.
@@ -1917,7 +1920,7 @@ export interface GetWithdrawalDetailsForUriOpts {
* Get more information about a taler://withdraw URI.
*
* As side effects, the bank (via the bank integration API) is queried
- * and the exchange suggested by the bank is permanently added
+ * and the exchange suggested by the bank is ephemerally added
* to the wallet's list of known exchanges.
*/
export async function getWithdrawalDetailsForUri(
@@ -1929,10 +1932,10 @@ export async function getWithdrawalDetailsForUri(
const info = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
logger.trace(`got bank info`);
if (info.suggestedExchange) {
- // FIXME: right now the exchange gets permanently added,
- // we might want to only temporarily add it.
try {
- await ws.exchangeOps.fetchFreshExchange(ws, info.suggestedExchange);
+ // If the exchange entry doesn't exist yet,
+ // it'll be created as an ephemeral entry.
+ await fetchFreshExchange(ws, info.suggestedExchange);
} catch (e) {
// We still continued if it failed, as other exchanges might be available.
// We don't want to fail if the bank-suggested exchange is broken/offline.
@@ -1942,40 +1945,12 @@ export async function getWithdrawalDetailsForUri(
}
}
- // Extract information about possible exchanges for the withdrawal
- // operation from the database.
-
- const exchanges: ExchangeListItem[] = [];
-
- await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.denominations,
- x.operationRetries,
- ])
- .runReadOnly(async (tx) => {
- const exchangeRecords = await tx.exchanges.iter().toArray();
- for (const r of exchangeRecords) {
- const exchangeDetails = await ws.exchangeOps.getExchangeDetails(
- tx,
- r.baseUrl,
- );
- const retryRecord = await tx.operationRetries.get(
- TaskIdentifiers.forExchangeUpdate(r),
- );
- if (exchangeDetails) {
- exchanges.push(
- makeExchangeListItem(r, exchangeDetails, retryRecord?.lastError),
- );
- }
- }
- });
+ const possibleExchangesResp = await listExchanges(ws);
return {
amount: Amounts.stringify(info.amount),
defaultExchangeBaseUrl: info.suggestedExchange,
- possibleExchanges: exchanges,
+ possibleExchanges: possibleExchangesResp.exchanges,
};
}
@@ -2005,7 +1980,7 @@ export async function getFundingPaytoUris(
): Promise<string[]> {
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
checkDbInvariant(!!withdrawalGroup);
- const exchangeDetails = await getExchangeDetails(
+ const exchangeDetails = await getExchangeWireDetailsInTx(
tx,
withdrawalGroup.exchangeBaseUrl,
);
@@ -2385,7 +2360,7 @@ export async function internalPrepareCreateWithdrawalGroup(
wgInfo: args.wgInfo,
};
- const exchangeInfo = await fetchFreshExchange(ws, canonExchange);
+ await fetchFreshExchange(ws, canonExchange);
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
@@ -2404,6 +2379,13 @@ export async function internalPrepareCreateWithdrawalGroup(
export interface PerformCreateWithdrawalGroupResult {
withdrawalGroup: WithdrawalGroupRecord;
transitionInfo: TransitionInfo | undefined;
+
+ /**
+ * Notification for the exchange state transition.
+ *
+ * Should be emitted after the transaction has succeeded.
+ */
+ exchangeNotif: WalletNotification | undefined;
}
export async function internalPerformCreateWithdrawalGroup(
@@ -2417,7 +2399,11 @@ export async function internalPerformCreateWithdrawalGroup(
): Promise<PerformCreateWithdrawalGroupResult> {
const { withdrawalGroup } = prep;
if (!prep.creationInfo) {
- return { withdrawalGroup, transitionInfo: undefined };
+ return {
+ withdrawalGroup,
+ transitionInfo: undefined,
+ exchangeNotif: undefined,
+ };
}
await tx.withdrawalGroups.add(withdrawalGroup);
await tx.reserves.put({
@@ -2428,7 +2414,6 @@ export async function internalPerformCreateWithdrawalGroup(
const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
if (exchange) {
exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now());
- exchange.entryStatus = ExchangeEntryDbRecordStatus.Used;
await tx.exchanges.put(exchange);
}
@@ -2442,7 +2427,17 @@ export async function internalPerformCreateWithdrawalGroup(
newTxState,
};
- return { withdrawalGroup, transitionInfo };
+ const exchangeUsedRes = await markExchangeUsed(
+ ws,
+ tx,
+ prep.withdrawalGroup.exchangeBaseUrl,
+ );
+
+ return {
+ withdrawalGroup,
+ transitionInfo,
+ exchangeNotif: exchangeUsedRes.notif,
+ };
}
/**
@@ -2481,6 +2476,9 @@ export async function internalCreateWithdrawalGroup(
.runReadWrite(async (tx) => {
return await internalPerformCreateWithdrawalGroup(ws, tx, prep);
});
+ if (res.exchangeNotif) {
+ ws.notify(res.exchangeNotif);
+ }
notifyTransition(ws, transactionId, res.transitionInfo);
return res.withdrawalGroup;
}
@@ -2535,10 +2533,7 @@ export async function acceptWithdrawalFromUri(
withdrawInfo.wireTypes,
);
- const exchange = await ws.exchangeOps.fetchFreshExchange(
- ws,
- selectedExchange,
- );
+ const exchange = await fetchFreshExchange(ws, selectedExchange);
const withdrawalAccountList = await fetchWithdrawalAccountInfo(ws, {
exchange,
@@ -2710,7 +2705,7 @@ export async function createManualWithdrawal(
): Promise<AcceptManualWithdrawalResult> {
const { exchangeBaseUrl } = req;
const amount = Amounts.parseOrThrow(req.amount);
- const exchange = await ws.exchangeOps.fetchFreshExchange(ws, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(ws, exchangeBaseUrl);
if (exchange.currency != amount.currency) {
throw Error(
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
index e3fbffe98..f24184609 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -57,7 +57,7 @@ import {
import { DenominationRecord } from "../db.js";
import {
getAutoRefreshExecuteThreshold,
- getExchangeDetails,
+ getExchangeWireDetailsInTx,
isWithdrawableDenom,
WalletDbReadOnlyTransaction,
} from "../index.js";
@@ -615,7 +615,10 @@ async function selectPayMerchantCandidates(
const exchanges = await tx.exchanges.iter().toArray();
const wfPerExchange: Record<string, AmountJson> = {};
for (const exchange of exchanges) {
- const exchangeDetails = await getExchangeDetails(tx, exchange.baseUrl);
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ exchange.baseUrl,
+ );
// 1.- exchange has same currency
if (exchangeDetails?.currency !== req.contractTermsAmount.currency) {
continue;
diff --git a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
index 4365e6d32..caa3fdca5 100644
--- a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
+++ b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
@@ -33,7 +33,7 @@ import {
import {
DenominationRecord,
InternalWalletState,
- getExchangeDetails,
+ getExchangeWireDetailsInTx,
timestampProtocolFromDb,
} from "../index.js";
import { CoinInfo } from "./coinSelection.js";
@@ -61,8 +61,8 @@ function getOperationType(txType: TransactionType): OperationType {
txType === TransactionType.Withdrawal
? OperationType.Credit
: txType === TransactionType.Deposit
- ? OperationType.Debit
- : undefined;
+ ? OperationType.Debit
+ : undefined;
if (!operationType) {
throw Error(`operation type ${txType} not yet supported`);
}
@@ -155,7 +155,10 @@ async function getAvailableDenoms(
filters.exchanges ?? databaseExchanges.map((e) => e.baseUrl);
for (const exchangeBaseUrl of filteredExchanges) {
- const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl);
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ exchangeBaseUrl,
+ );
// 1.- exchange has same currency
if (exchangeDetails?.currency !== currency) {
continue;
@@ -221,9 +224,10 @@ async function getAvailableDenoms(
//4.- filter coins restricted by age
if (operationType === OperationType.Credit) {
// FIXME: Use denom groups instead of querying all denominations!
- const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
- exchangeBaseUrl,
- );
+ const ds =
+ await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ exchangeBaseUrl,
+ );
for (const denom of ds) {
const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
timestampProtocolFromDb(denom.stampExpireWithdraw),
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 154665c47..ff1f991dd 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -41,7 +41,6 @@ import {
KnownBankAccounts,
KnownBankAccountsInfo,
Logger,
- WithdrawalDetailsForAmount,
MerchantUsingTemplateDetails,
NotificationType,
PrepareWithdrawExchangeRequest,
@@ -61,6 +60,7 @@ import {
ValidateIbanResponse,
WalletCoreVersion,
WalletNotification,
+ WithdrawalDetailsForAmount,
codecForAbortTransaction,
codecForAcceptBankIntegratedWithdrawalRequest,
codecForAcceptExchangeTosRequest,
@@ -157,7 +157,6 @@ import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
import {
ActiveLongpollInfo,
CancelFn,
- ExchangeOperations,
InternalWalletState,
MerchantInfo,
MerchantOperations,
@@ -185,10 +184,8 @@ import {
} from "./operations/backup/index.js";
import { getBalanceDetail, getBalances } from "./operations/balance.js";
import {
- TaskIdentifiers,
TaskRunResult,
TaskRunResultType,
- makeExchangeListItem,
runTaskWithErrorReporting,
} from "./operations/common.js";
import {
@@ -203,9 +200,9 @@ import {
addPresetExchangeEntry,
fetchFreshExchange,
getExchangeDetailedInfo,
- getExchangeDetails,
getExchangeTos,
listExchanges,
+ lookupExchangeByUri,
updateExchangeFromUrlHandler,
} from "./operations/exchanges.js";
import { getMerchantInfo } from "./operations/merchants.js";
@@ -967,32 +964,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
}
case WalletApiOperation.GetExchangeEntryByUrl: {
const req = codecForGetExchangeEntryByUrlRequest().decode(payload);
- const exchangeEntry = await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.denominations,
- x.operationRetries,
- ])
- .runReadOnly(async (tx) => {
- const exchangeRec = await tx.exchanges.get(req.exchangeBaseUrl);
- if (!exchangeRec) {
- throw Error("exchange not found");
- }
- const exchangeDetails = await getExchangeDetails(
- tx,
- exchangeRec.baseUrl,
- );
- const opRetryRecord = await tx.operationRetries.get(
- TaskIdentifiers.forExchangeUpdate(exchangeRec),
- );
- return makeExchangeListItem(
- exchangeRec,
- exchangeDetails,
- opRetryRecord?.lastError,
- );
- });
- return exchangeEntry;
+ return lookupExchangeByUri(ws, req);
}
case WalletApiOperation.ListExchangesForScopedCurrency: {
const req =
@@ -1687,11 +1659,6 @@ class InternalWalletStateImpl implements InternalWalletState {
initCalled = false;
- exchangeOps: ExchangeOperations = {
- getExchangeDetails,
- fetchFreshExchange,
- };
-
recoupOps: RecoupOperations = {
createRecoupGroup,
};