aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-06-13 18:19:20 +0200
committerFlorian Dold <florian@dold.me>2024-06-13 18:22:23 +0200
commit10aa5e767a35d39d6612f5e4addf4e04f3241a42 (patch)
tree70d62dbbe02a9257e0af988a09de8cc0ee9ba557 /packages
parent955b957ef6d7d27d444d363cf80ea8942233f97c (diff)
downloadwallet-core-10aa5e767a35d39d6612f5e4addf4e04f3241a42.tar.xz
wallet-core: introduce coin history store, spend coins transactionally
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts373
-rw-r--r--packages/taler-wallet-core/src/db.ts26
-rw-r--r--packages/taler-wallet-core/src/deposits.ts65
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts284
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts77
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts81
-rw-r--r--packages/taler-wallet-core/src/transactions.ts9
7 files changed, 508 insertions, 407 deletions
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index db6384c93..51316a21f 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -252,6 +252,88 @@ async function internalSelectPayCoins(
};
}
+export async function selectPayCoinsInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "coins",
+ ]
+ >,
+ req: SelectPayCoinRequestNg,
+): Promise<SelectPayCoinsResult> {
+ if (logger.shouldLogTrace()) {
+ logger.trace(`selecting coins for ${j2s(req)}`);
+ }
+
+ const materialAvSel = await internalSelectPayCoins(wex, tx, req, false);
+
+ if (!materialAvSel) {
+ const prospectiveAvSel = await internalSelectPayCoins(wex, tx, req, true);
+
+ if (prospectiveAvSel) {
+ const prospectiveCoins: SelectedProspectiveCoin[] = [];
+ for (const avKey of Object.keys(prospectiveAvSel.sel)) {
+ const mySel = prospectiveAvSel.sel[avKey];
+ for (const contrib of mySel.contributions) {
+ prospectiveCoins.push({
+ denomPubHash: mySel.denomPubHash,
+ contribution: Amounts.stringify(contrib),
+ exchangeBaseUrl: mySel.exchangeBaseUrl,
+ });
+ }
+ }
+ return {
+ type: "prospective",
+ result: {
+ prospectiveCoins,
+ customerDepositFees: Amounts.stringify(
+ prospectiveAvSel.tally.customerDepositFees,
+ ),
+ customerWireFees: Amounts.stringify(
+ prospectiveAvSel.tally.customerWireFees,
+ ),
+ },
+ } satisfies SelectPayCoinsResult;
+ }
+
+ return {
+ type: "failure",
+ insufficientBalanceDetails: await reportInsufficientBalanceDetails(
+ wex,
+ tx,
+ {
+ restrictExchanges: req.restrictExchanges,
+ instructedAmount: req.contractTermsAmount,
+ requiredMinimumAge: req.requiredMinimumAge,
+ wireMethod: req.restrictWireMethod,
+ depositPaytoUri: req.depositPaytoUri,
+ },
+ ),
+ } satisfies SelectPayCoinsResult;
+ }
+
+ const coinSel = await assembleSelectPayCoinsSuccessResult(
+ tx,
+ materialAvSel.sel,
+ materialAvSel.coinRes,
+ materialAvSel.tally,
+ );
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`coin selection: ${j2s(coinSel)}`);
+ }
+
+ return {
+ type: "success",
+ coinSel,
+ };
+}
+
/**
* Select coins to spend under the merchant's constraints.
*
@@ -263,10 +345,6 @@ export async function selectPayCoins(
wex: WalletExecutionContext,
req: SelectPayCoinRequestNg,
): Promise<SelectPayCoinsResult> {
- if (logger.shouldLogTrace()) {
- logger.trace(`selecting coins for ${j2s(req)}`);
- }
-
return await wex.db.runReadOnlyTx(
{
storeNames: [
@@ -279,73 +357,7 @@ export async function selectPayCoins(
],
},
async (tx) => {
- const materialAvSel = await internalSelectPayCoins(wex, tx, req, false);
-
- if (!materialAvSel) {
- const prospectiveAvSel = await internalSelectPayCoins(
- wex,
- tx,
- req,
- true,
- );
-
- if (prospectiveAvSel) {
- const prospectiveCoins: SelectedProspectiveCoin[] = [];
- for (const avKey of Object.keys(prospectiveAvSel.sel)) {
- const mySel = prospectiveAvSel.sel[avKey];
- for (const contrib of mySel.contributions) {
- prospectiveCoins.push({
- denomPubHash: mySel.denomPubHash,
- contribution: Amounts.stringify(contrib),
- exchangeBaseUrl: mySel.exchangeBaseUrl,
- });
- }
- }
- return {
- type: "prospective",
- result: {
- prospectiveCoins,
- customerDepositFees: Amounts.stringify(
- prospectiveAvSel.tally.customerDepositFees,
- ),
- customerWireFees: Amounts.stringify(
- prospectiveAvSel.tally.customerWireFees,
- ),
- },
- } satisfies SelectPayCoinsResult;
- }
-
- return {
- type: "failure",
- insufficientBalanceDetails: await reportInsufficientBalanceDetails(
- wex,
- tx,
- {
- restrictExchanges: req.restrictExchanges,
- instructedAmount: req.contractTermsAmount,
- requiredMinimumAge: req.requiredMinimumAge,
- wireMethod: req.restrictWireMethod,
- depositPaytoUri: req.depositPaytoUri,
- },
- ),
- } satisfies SelectPayCoinsResult;
- }
-
- const coinSel = await assembleSelectPayCoinsSuccessResult(
- tx,
- materialAvSel.sel,
- materialAvSel.coinRes,
- materialAvSel.tally,
- );
-
- if (logger.shouldLogTrace()) {
- logger.trace(`coin selection: ${j2s(coinSel)}`);
- }
-
- return {
- type: "success",
- coinSel,
- };
+ return selectPayCoinsInTx(wex, tx, req);
},
);
}
@@ -910,7 +922,10 @@ async function selectPayCandidates(
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
- checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`,
+ );
if (denom.isRevoked) {
logger.trace("denom is revoked");
continue;
@@ -1131,17 +1146,127 @@ async function internalSelectPeerCoins(
};
}
-export async function selectPeerCoins(
+export async function selectPeerCoinsInTx(
wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchangeDetails",
+ ]
+ >,
req: PeerCoinSelectionRequest,
): Promise<SelectPeerCoinsResult> {
const instructedAmount = req.instructedAmount;
if (Amounts.isZero(instructedAmount)) {
// Other parts of the code assume that we have at least
// one coin to spend.
- throw new Error("amount of zero not allowed");
+ throw new Error("peer-to-peer payment with amount of zero not supported");
}
+ const exchanges = await tx.exchanges.iter().toArray();
+ const currency = Amounts.currencyOf(instructedAmount);
+ for (const exch of exchanges) {
+ if (exch.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ const exchWire = await getExchangeWireDetailsInTx(tx, exch.baseUrl);
+ if (!exchWire) {
+ continue;
+ }
+ const globalFees = getGlobalFees(exchWire);
+ if (!globalFees) {
+ continue;
+ }
+
+ const avRes = await internalSelectPeerCoins(wex, tx, req, exchWire, false);
+
+ if (!avRes) {
+ // Try to see if we can do a prospective selection
+ const prospectiveAvRes = await internalSelectPeerCoins(
+ wex,
+ tx,
+ req,
+ exchWire,
+ true,
+ );
+ if (prospectiveAvRes) {
+ const prospectiveCoins: SelectedProspectiveCoin[] = [];
+ for (const avKey of Object.keys(prospectiveAvRes.sel)) {
+ const mySel = prospectiveAvRes.sel[avKey];
+ for (const contrib of mySel.contributions) {
+ prospectiveCoins.push({
+ denomPubHash: mySel.denomPubHash,
+ contribution: Amounts.stringify(contrib),
+ exchangeBaseUrl: mySel.exchangeBaseUrl,
+ });
+ }
+ }
+ const maxExpirationDate = await computeCoinSelMaxExpirationDate(
+ wex,
+ tx,
+ prospectiveAvRes.sel,
+ );
+ return {
+ type: "prospective",
+ result: {
+ prospectiveCoins,
+ depositFees: prospectiveAvRes.tally.customerDepositFees,
+ exchangeBaseUrl: exch.baseUrl,
+ maxExpirationDate,
+ },
+ };
+ }
+ } else if (avRes) {
+ const r = await assembleSelectPayCoinsSuccessResult(
+ tx,
+ avRes.sel,
+ avRes.resCoins,
+ avRes.tally,
+ );
+
+ const maxExpirationDate = await computeCoinSelMaxExpirationDate(
+ wex,
+ tx,
+ avRes.sel,
+ );
+
+ return {
+ type: "success",
+ result: {
+ coins: r.coins,
+ depositFees: Amounts.parseOrThrow(r.customerDepositFees),
+ exchangeBaseUrl: exch.baseUrl,
+ maxExpirationDate,
+ },
+ };
+ }
+ }
+ const insufficientBalanceDetails = await reportInsufficientBalanceDetails(
+ wex,
+ tx,
+ {
+ restrictExchanges: undefined,
+ instructedAmount: req.instructedAmount,
+ requiredMinimumAge: undefined,
+ wireMethod: undefined,
+ depositPaytoUri: undefined,
+ },
+ );
+ return {
+ type: "failure",
+ insufficientBalanceDetails,
+ };
+}
+
+export async function selectPeerCoins(
+ wex: WalletExecutionContext,
+ req: PeerCoinSelectionRequest,
+): Promise<SelectPeerCoinsResult> {
return await wex.db.runReadWriteTx(
{
storeNames: [
@@ -1155,105 +1280,7 @@ export async function selectPeerCoins(
],
},
async (tx): Promise<SelectPeerCoinsResult> => {
- const exchanges = await tx.exchanges.iter().toArray();
- const currency = Amounts.currencyOf(instructedAmount);
- for (const exch of exchanges) {
- if (exch.detailsPointer?.currency !== currency) {
- continue;
- }
- const exchWire = await getExchangeWireDetailsInTx(tx, exch.baseUrl);
- if (!exchWire) {
- continue;
- }
- const globalFees = getGlobalFees(exchWire);
- if (!globalFees) {
- continue;
- }
-
- const avRes = await internalSelectPeerCoins(
- wex,
- tx,
- req,
- exchWire,
- false,
- );
-
- if (!avRes) {
- // Try to see if we can do a prospective selection
- const prospectiveAvRes = await internalSelectPeerCoins(
- wex,
- tx,
- req,
- exchWire,
- true,
- );
- if (prospectiveAvRes) {
- const prospectiveCoins: SelectedProspectiveCoin[] = [];
- for (const avKey of Object.keys(prospectiveAvRes.sel)) {
- const mySel = prospectiveAvRes.sel[avKey];
- for (const contrib of mySel.contributions) {
- prospectiveCoins.push({
- denomPubHash: mySel.denomPubHash,
- contribution: Amounts.stringify(contrib),
- exchangeBaseUrl: mySel.exchangeBaseUrl,
- });
- }
- }
- const maxExpirationDate = await computeCoinSelMaxExpirationDate(
- wex,
- tx,
- prospectiveAvRes.sel,
- );
- return {
- type: "prospective",
- result: {
- prospectiveCoins,
- depositFees: prospectiveAvRes.tally.customerDepositFees,
- exchangeBaseUrl: exch.baseUrl,
- maxExpirationDate,
- },
- };
- }
- } else if (avRes) {
- const r = await assembleSelectPayCoinsSuccessResult(
- tx,
- avRes.sel,
- avRes.resCoins,
- avRes.tally,
- );
-
- const maxExpirationDate = await computeCoinSelMaxExpirationDate(
- wex,
- tx,
- avRes.sel,
- );
-
- return {
- type: "success",
- result: {
- coins: r.coins,
- depositFees: Amounts.parseOrThrow(r.customerDepositFees),
- exchangeBaseUrl: exch.baseUrl,
- maxExpirationDate,
- },
- };
- }
- }
- const insufficientBalanceDetails = await reportInsufficientBalanceDetails(
- wex,
- tx,
- {
- restrictExchanges: undefined,
- instructedAmount: req.instructedAmount,
- requiredMinimumAge: undefined,
- wireMethod: undefined,
- depositPaytoUri: undefined,
- },
- );
- return {
- type: "failure",
- insufficientBalanceDetails,
- };
+ return selectPeerCoinsInTx(wex, tx, req);
},
);
}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 5c381eea7..0ce838fd2 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -912,6 +912,27 @@ export interface CoinRecord {
ageCommitmentProof: AgeCommitmentProof | undefined;
}
+export type HistoryItem =
+ | {
+ type: "withdraw";
+ transactionId: TransactionIdStr;
+ }
+ | { type: "spend"; transactionId: TransactionIdStr; amount: AmountString }
+ | { type: "refresh"; transactionId: TransactionIdStr; amount: AmountString }
+ | { type: "recoup"; transactionId: TransactionIdStr; amount: AmountString }
+ | { type: "refund"; transactionId: TransactionIdStr; amount: AmountString };
+
+/**
+ * History event for a coin from the wallet's perspective.
+ */
+export interface CoinHistoryRecord {
+ coinPub: string;
+
+ timestamp: DbPreciseTimestamp;
+
+ item: HistoryItem;
+}
+
/**
* Coin allocation, i.e. what a coin has been used for.
*/
@@ -2423,6 +2444,11 @@ export const WalletStoresV1 = {
}),
},
),
+ coinHistory: describeStoreV2({
+ storeName: "coinHistory",
+ recordCodec: passthroughCodec<CoinHistoryRecord>(),
+ keyPath: ["coinPub", "timestamp"],
+ }),
coins: describeStore(
"coins",
describeContents<CoinRecord>({
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index 2004c12cb..23b52ac5c 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -72,7 +72,7 @@ import {
stringToBytes,
} from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
-import { selectPayCoins } from "./coinSelection.js";
+import { selectPayCoins, selectPayCoinsInTx } from "./coinSelection.js";
import {
PendingTaskType,
TaskIdStr,
@@ -979,40 +979,12 @@ async function processDepositGroupPendingDeposit(
if (!depositGroup.payCoinSelection) {
logger.info("missing coin selection for deposit group, selecting now");
- // FIXME: Consider doing the coin selection inside the txn
- const payCoinSel = await selectPayCoins(wex, {
- restrictExchanges: {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- },
- restrictWireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- prevPayCoins: [],
- });
-
- switch (payCoinSel.type) {
- case "success":
- logger.info("coin selection success");
- break;
- case "failure":
- logger.info("coin selection failure");
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
- },
- );
- case "prospective":
- logger.info("coin selection prospective");
- throw Error("insufficient balance (waiting on pending refresh)");
- default:
- assertUnreachable(payCoinSel);
- }
const transitionDone = await wex.db.runReadWriteTx(
{
storeNames: [
+ "exchanges",
+ "exchangeDetails",
"depositGroups",
"coins",
"coinAvailability",
@@ -1029,6 +1001,37 @@ async function processDepositGroupPendingDeposit(
if (dg.statusPerCoin) {
return false;
}
+ const payCoinSel = await selectPayCoinsInTx(wex, tx, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ });
+
+ switch (payCoinSel.type) {
+ case "success":
+ logger.info("coin selection success");
+ break;
+ case "failure":
+ logger.info("coin selection failure");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails:
+ payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ logger.info("coin selection prospective");
+ throw Error("insufficient balance (waiting on pending refresh)");
+ default:
+ assertUnreachable(payCoinSel);
+ }
+
dg.payCoinSelection = {
coinContributions: payCoinSel.coinSel.coins.map(
(x) => x.contribution,
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
index ee154252f..993b12dd1 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -101,7 +101,11 @@ import {
readUnexpectedResponseDetails,
throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
-import { PreviousPayCoins, selectPayCoins } from "./coinSelection.js";
+import {
+ PreviousPayCoins,
+ selectPayCoins,
+ selectPayCoinsInTx,
+} from "./coinSelection.js";
import {
constructTaskIdentifier,
PendingTaskType,
@@ -472,33 +476,42 @@ export async function getTotalPaymentCost(
return wex.db.runReadOnlyTx(
{ storeNames: ["coins", "denominations"] },
async (tx) => {
- const costs: AmountJson[] = [];
- for (let i = 0; i < pcs.length; i++) {
- const denom = await tx.denominations.get([
- pcs[i].exchangeBaseUrl,
- pcs[i].denomPubHash,
- ]);
- if (!denom) {
- throw Error(
- "can't calculate payment cost, denomination for coin not found",
- );
- }
- const amountLeft = Amounts.sub(denom.value, pcs[i].contribution).amount;
- const refreshCost = await getTotalRefreshCost(
- wex,
- tx,
- DenominationRecord.toDenomInfo(denom),
- amountLeft,
- );
- costs.push(Amounts.parseOrThrow(pcs[i].contribution));
- costs.push(refreshCost);
- }
- const zero = Amounts.zeroOfCurrency(currency);
- return Amounts.sum([zero, ...costs]).amount;
+ return getTotalPaymentCostInTx(wex, tx, currency, pcs);
},
);
}
+export async function getTotalPaymentCostInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
+ currency: string,
+ pcs: SelectedProspectiveCoin[],
+): Promise<AmountJson> {
+ const costs: AmountJson[] = [];
+ for (let i = 0; i < pcs.length; i++) {
+ const denom = await tx.denominations.get([
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
+ ]);
+ if (!denom) {
+ throw Error(
+ "can't calculate payment cost, denomination for coin not found",
+ );
+ }
+ const amountLeft = Amounts.sub(denom.value, pcs[i].contribution).amount;
+ const refreshCost = await getTotalRefreshCost(
+ wex,
+ tx,
+ DenominationRecord.toDenomInfo(denom),
+ amountLeft,
+ );
+ costs.push(Amounts.parseOrThrow(pcs[i].contribution));
+ costs.push(refreshCost);
+ }
+ const zero = Amounts.zeroOfCurrency(currency);
+ return Amounts.sum([zero, ...costs]).amount;
+}
+
async function failProposalPermanently(
wex: WalletExecutionContext,
proposalId: string,
@@ -533,13 +546,10 @@ function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
);
}
-/**
- * Return the proposal download data for a purchase, throw if not available.
- */
-export async function expectProposalDownload(
+export async function expectProposalDownloadInTx(
wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["contractTerms"]>,
p: PurchaseRecord,
- parentTx?: WalletDbReadOnlyTransaction<["contractTerms"]>,
): Promise<{
contractData: WalletContractData;
contractTermsRaw: any;
@@ -549,31 +559,35 @@ export async function expectProposalDownload(
}
const download = p.download;
- async function getFromTransaction(
- tx: Exclude<typeof parentTx, undefined>,
- ): Promise<ReturnType<typeof expectProposalDownload>> {
- const contractTerms = await tx.contractTerms.get(
- download.contractTermsHash,
- );
- if (!contractTerms) {
- throw Error("contract terms not found");
- }
- return {
- contractData: extractContractData(
- contractTerms.contractTermsRaw,
- download.contractTermsHash,
- download.contractTermsMerchantSig,
- ),
- contractTermsRaw: contractTerms.contractTermsRaw,
- };
+ const contractTerms = await tx.contractTerms.get(download.contractTermsHash);
+ if (!contractTerms) {
+ throw Error("contract terms not found");
}
+ return {
+ contractData: extractContractData(
+ contractTerms.contractTermsRaw,
+ download.contractTermsHash,
+ download.contractTermsMerchantSig,
+ ),
+ contractTermsRaw: contractTerms.contractTermsRaw,
+ };
+}
- if (parentTx) {
- return getFromTransaction(parentTx);
- }
+/**
+ * Return the proposal download data for a purchase, throw if not available.
+ */
+export async function expectProposalDownload(
+ wex: WalletExecutionContext,
+ p: PurchaseRecord,
+): Promise<{
+ contractData: WalletContractData;
+ contractTermsRaw: any;
+}> {
return await wex.db.runReadOnlyTx(
{ storeNames: ["contractTerms"] },
- getFromTransaction,
+ async (tx) => {
+ return expectProposalDownloadInTx(wex, tx, p);
+ },
);
}
@@ -1148,8 +1162,6 @@ async function handleInsufficientFunds(
throw new TalerProtocolViolationError();
}
- const { contractData } = await expectProposalDownload(wex, proposal);
-
const prevPayCoins: PreviousPayCoins = [];
const payInfo = proposal.payInfo;
@@ -1162,49 +1174,14 @@ async function handleInsufficientFunds(
return;
}
- await wex.db.runReadOnlyTx(
- { storeNames: ["coins", "denominations"] },
- async (tx) => {
- for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
- const coinPub = payCoinSelection.coinPubs[i];
- const contrib = payCoinSelection.coinContributions[i];
- prevPayCoins.push({
- coinPub,
- contribution: Amounts.parseOrThrow(contrib),
- });
- }
- },
- );
-
- const res = await selectPayCoins(wex, {
- restrictExchanges: {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- },
- restrictWireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- prevPayCoins,
- requiredMinimumAge: contractData.minimumAge,
- });
-
- switch (res.type) {
- case "failure":
- logger.trace("insufficient funds for coin re-selection");
- return;
- case "prospective":
- return;
- case "success":
- break;
- default:
- assertUnreachable(res);
- }
-
- logger.trace("re-selected coins");
+ // FIXME: Above code should go into the transaction.
await wex.db.runReadWriteTx(
{
storeNames: [
+ "contractTerms",
+ "exchanges",
+ "exchangeDetails",
"purchases",
"coins",
"coinAvailability",
@@ -1222,6 +1199,46 @@ async function handleInsufficientFunds(
if (!payInfo) {
return;
}
+
+ const { contractData } = await expectProposalDownloadInTx(
+ wex,
+ tx,
+ proposal,
+ );
+
+ for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
+ const coinPub = payCoinSelection.coinPubs[i];
+ const contrib = payCoinSelection.coinContributions[i];
+ prevPayCoins.push({
+ coinPub,
+ contribution: Amounts.parseOrThrow(contrib),
+ });
+ }
+
+ const res = await selectPayCoinsInTx(wex, tx, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins,
+ requiredMinimumAge: contractData.minimumAge,
+ });
+
+ switch (res.type) {
+ case "failure":
+ logger.trace("insufficient funds for coin re-selection");
+ return;
+ case "prospective":
+ return;
+ case "success":
+ break;
+ default:
+ assertUnreachable(res);
+ }
+
// Convert to DB format
payInfo.payCoinSelection = {
coinContributions: res.coinSel.coins.map((x) => x.contribution),
@@ -1936,44 +1953,6 @@ export async function confirmPay(
const currency = Amounts.currencyOf(contractData.amount);
- const selectCoinsResult = await selectPayCoins(wex, {
- restrictExchanges: {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- },
- restrictWireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- prevPayCoins: [],
- requiredMinimumAge: contractData.minimumAge,
- forcedSelection: forcedCoinSel,
- });
-
- let coins: SelectedProspectiveCoin[] | undefined = undefined;
-
- switch (selectCoinsResult.type) {
- case "failure": {
- // Should not happen, since checkPay should be called first
- // FIXME: Actually, this should be handled gracefully,
- // and the status should be stored in the DB.
- logger.warn("not confirming payment, insufficient coins");
- throw Error("insufficient balance");
- }
- case "prospective": {
- coins = selectCoinsResult.result.prospectiveCoins;
- break;
- }
- case "success":
- coins = selectCoinsResult.coinSel.coins;
- break;
- default:
- assertUnreachable(selectCoinsResult);
- }
-
- logger.trace("coin selection result", selectCoinsResult);
-
- const payCostInfo = await getTotalPaymentCost(wex, currency, coins);
-
let sessionId: string | undefined;
if (sessionIdOverride) {
sessionId = sessionIdOverride;
@@ -1988,6 +1967,8 @@ export async function confirmPay(
const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
+ "exchanges",
+ "exchangeDetails",
"purchases",
"coins",
"refreshGroups",
@@ -2001,6 +1982,50 @@ export async function confirmPay(
if (!p) {
return;
}
+
+ const selectCoinsResult = await selectPayCoinsInTx(wex, tx, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ requiredMinimumAge: contractData.minimumAge,
+ forcedSelection: forcedCoinSel,
+ });
+
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (selectCoinsResult.type) {
+ case "failure": {
+ // Should not happen, since checkPay should be called first
+ // FIXME: Actually, this should be handled gracefully,
+ // and the status should be stored in the DB.
+ logger.warn("not confirming payment, insufficient coins");
+ throw Error("insufficient balance");
+ }
+ case "prospective": {
+ coins = selectCoinsResult.result.prospectiveCoins;
+ break;
+ }
+ case "success":
+ coins = selectCoinsResult.coinSel.coins;
+ break;
+ default:
+ assertUnreachable(selectCoinsResult);
+ }
+
+ logger.trace("coin selection result", selectCoinsResult);
+
+ const payCostInfo = await getTotalPaymentCostInTx(
+ wex,
+ tx,
+ currency,
+ coins,
+ );
+
const oldTxState = computePayMerchantTransactionState(p);
switch (p.purchaseStatus) {
case PurchaseStatus.DialogShared:
@@ -2036,7 +2061,6 @@ export async function confirmPay(
refreshReason: RefreshReason.PayMerchant,
});
}
-
break;
case PurchaseStatus.Done:
case PurchaseStatus.PendingPaying:
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts
index a1729ced7..636dd4156 100644
--- a/packages/taler-wallet-core/src/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -31,7 +31,11 @@ import {
codecOptional,
} from "@gnu-taler/taler-util";
import { SpendCoinDetails } from "./crypto/cryptoImplementation.js";
-import { DbPeerPushPaymentCoinSelection, ReserveRecord } from "./db.js";
+import {
+ DbPeerPushPaymentCoinSelection,
+ ReserveRecord,
+ WalletDbReadOnlyTransaction,
+} from "./db.js";
import { getTotalRefreshCost } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
@@ -74,6 +78,38 @@ export async function queryCoinInfosForSelection(
return infos;
}
+export async function getTotalPeerPaymentCostInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
+ pcs: SelectedProspectiveCoin[],
+): Promise<AmountJson> {
+ const costs: AmountJson[] = [];
+ for (let i = 0; i < pcs.length; i++) {
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
+ );
+ if (!denomInfo) {
+ throw Error(
+ "can't calculate payment cost, denomination for coin not found",
+ );
+ }
+ const amountLeft = Amounts.sub(denomInfo.value, pcs[i].contribution).amount;
+ const refreshCost = await getTotalRefreshCost(
+ wex,
+ tx,
+ denomInfo,
+ amountLeft,
+ );
+ costs.push(Amounts.parseOrThrow(pcs[i].contribution));
+ costs.push(refreshCost);
+ }
+ const zero = Amounts.zeroOfAmount(pcs[0].contribution);
+ return Amounts.sum([zero, ...costs]).amount;
+}
+
export async function getTotalPeerPaymentCost(
wex: WalletExecutionContext,
pcs: SelectedProspectiveCoin[],
@@ -81,34 +117,7 @@ export async function getTotalPeerPaymentCost(
return wex.db.runReadOnlyTx(
{ storeNames: ["coins", "denominations"] },
async (tx) => {
- const costs: AmountJson[] = [];
- for (let i = 0; i < pcs.length; i++) {
- const denomInfo = await getDenomInfo(
- wex,
- tx,
- pcs[i].exchangeBaseUrl,
- pcs[i].denomPubHash,
- );
- if (!denomInfo) {
- throw Error(
- "can't calculate payment cost, denomination for coin not found",
- );
- }
- const amountLeft = Amounts.sub(
- denomInfo.value,
- pcs[i].contribution,
- ).amount;
- const refreshCost = await getTotalRefreshCost(
- wex,
- tx,
- denomInfo,
- amountLeft,
- );
- costs.push(Amounts.parseOrThrow(pcs[i].contribution));
- costs.push(refreshCost);
- }
- const zero = Amounts.zeroOfAmount(pcs[0].contribution);
- return Amounts.sum([zero, ...costs]).amount;
+ return getTotalPeerPaymentCostInTx(wex, tx, pcs);
},
);
}
@@ -143,7 +152,10 @@ export async function getMergeReserveInfo(
checkDbInvariant(!!ex, `no exchange record for ${req.exchangeBaseUrl}`);
if (ex.currentMergeReserveRowId != null) {
const reserve = await tx.reserves.get(ex.currentMergeReserveRowId);
- checkDbInvariant(!!reserve, `reserver ${ex.currentMergeReserveRowId} missing in db`);
+ checkDbInvariant(
+ !!reserve,
+ `reserver ${ex.currentMergeReserveRowId} missing in db`,
+ );
return reserve;
}
const reserve: ReserveRecord = {
@@ -151,7 +163,10 @@ export async function getMergeReserveInfo(
reservePub: newReservePair.pub,
};
const insertResp = await tx.reserves.put(reserve);
- checkDbInvariant(typeof insertResp.key === "number", `reserve key is not a number`);
+ checkDbInvariant(
+ typeof insertResp.key === "number",
+ `reserve key is not a number`,
+ );
reserve.rowId = insertResp.key;
ex.currentMergeReserveRowId = reserve.rowId;
await tx.exchanges.put(ex);
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
index f8e6adb3c..9c31de06a 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -51,7 +51,11 @@ import {
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
-import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js";
+import {
+ PreviousPayCoins,
+ selectPeerCoins,
+ selectPeerCoinsInTx,
+} from "./coinSelection.js";
import {
PendingTaskType,
TaskIdStr,
@@ -73,6 +77,7 @@ import {
import {
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
+ getTotalPeerPaymentCostInTx,
queryCoinInfosForSelection,
} from "./pay-peer-common.js";
import { createRefreshGroup, waitRefreshFinal } from "./refresh.js";
@@ -1089,39 +1094,6 @@ export async function initiatePeerPushDebit(
const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({});
- const coinSelRes = await selectPeerCoins(wex, {
- instructedAmount,
- });
-
- let coins: SelectedProspectiveCoin[] | undefined = undefined;
-
- switch (coinSelRes.type) {
- case "failure":
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
- },
- );
- case "prospective":
- coins = coinSelRes.result.prospectiveCoins;
- break;
- case "success":
- coins = coinSelRes.result.coins;
- break;
- default:
- assertUnreachable(coinSelRes);
- }
-
- const sel = coinSelRes.result;
-
- logger.info(`selected p2p coins (push):`);
- logger.trace(`${j2s(coinSelRes)}`);
-
- const totalAmount = await getTotalPeerPaymentCost(wex, coins);
-
- logger.info(`computed total peer payment cost`);
-
const pursePub = pursePair.pub;
const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
@@ -1130,10 +1102,11 @@ export async function initiatePeerPushDebit(
const contractEncNonce = encodeCrock(getRandomBytes(24));
- const transitionInfo = await wex.db.runReadWriteTx(
+ const res = await wex.db.runReadWriteTx(
{
storeNames: [
"exchanges",
+ "exchangeDetails",
"contractTerms",
"coins",
"coinAvailability",
@@ -1144,6 +1117,33 @@ export async function initiatePeerPushDebit(
],
},
async (tx) => {
+ const coinSelRes = await selectPeerCoinsInTx(wex, tx, {
+ instructedAmount,
+ });
+
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+
+ const sel = coinSelRes.result;
+
+ const totalAmount = await getTotalPeerPaymentCostInTx(wex, tx, coins);
const ppi: PeerPushDebitRecord = {
amount: Amounts.stringify(instructedAmount),
contractPriv: contractKeyPair.priv,
@@ -1191,12 +1191,15 @@ export async function initiatePeerPushDebit(
const newTxState = computePeerPushDebitTransactionState(ppi);
return {
- oldTxState: { major: TransactionMajorState.None },
- newTxState,
+ transitionInfo: {
+ oldTxState: { major: TransactionMajorState.None },
+ newTxState,
+ },
+ exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, transactionId, res.transitionInfo);
wex.ws.notify({
type: NotificationType.BalanceChange,
hintTransactionId: transactionId,
@@ -1208,7 +1211,7 @@ export async function initiatePeerPushDebit(
contractPriv: contractKeyPair.priv,
mergePriv: mergePair.priv,
pursePub: pursePair.pub,
- exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
+ exchangeBaseUrl: res.exchangeBaseUrl,
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub: pursePair.pub,
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
index 7782d09ba..7f766f1b0 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -99,7 +99,7 @@ import {
computePayMerchantTransactionActions,
computePayMerchantTransactionState,
computeRefundTransactionState,
- expectProposalDownload,
+ expectProposalDownloadInTx,
extractContractData,
PayMerchantTransactionContext,
RefundTransactionContext,
@@ -306,7 +306,7 @@ export async function getTransactionById(
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) throw Error("not found");
- const download = await expectProposalDownload(wex, purchase, tx);
+ const download = await expectProposalDownloadInTx(wex, tx, purchase);
const contractData = download.contractData;
const payOpId = TaskIdentifiers.forPay(purchase);
const payRetryRecord = await tx.operationRetries.get(payOpId);
@@ -744,7 +744,10 @@ function buildTransactionForBankIntegratedWithdraw(
? undefined
: Amounts.currencyOf(wg.instructedAmount);
const currency = wg.wgInfo.bankInfo.currency ?? instructedCurrency;
- checkDbInvariant(currency !== undefined, "wg uninitialized (missing currency)");
+ checkDbInvariant(
+ currency !== undefined,
+ "wg uninitialized (missing currency)",
+ );
const txState = computeWithdrawalTransactionStatus(wg);
const zero = Amounts.stringify(Amounts.zeroOfCurrency(currency));