aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-01-13 02:24:19 +0100
committerFlorian Dold <florian@dold.me>2023-01-13 02:24:19 +0100
commita31b8c3c3105d0ba11f2a1c513c6b6bec3ebeb49 (patch)
treebeae1051da582dca7dc2b6e01b2dc997279f31e9
parenta3f9e8680501a3090d88220bf5a09681604c4471 (diff)
downloadwallet-core-a31b8c3c3105d0ba11f2a1c513c6b6bec3ebeb49.tar.xz
wallet-core: store total p2p push cost in DB
-rw-r--r--packages/taler-wallet-core/src/db.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer.ts389
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts2
3 files changed, 196 insertions, 197 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 9ca88d086..adf704bc4 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1713,6 +1713,8 @@ export interface PeerPushPaymentInitiationRecord {
*/
amount: AmountString;
+ totalCost: AmountString;
+
coinSel: PeerPushPaymentCoinSelection;
contractTermsHash: HashCodeString;
diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts
index 8dc468ad9..bb36217d6 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer.ts
@@ -103,20 +103,22 @@ import { internalCreateWithdrawalGroup } from "./withdraw.js";
const logger = new Logger("operations/peer-to-peer.ts");
-export interface PeerCoinSelectionDetails {
+interface SelectedPeerCoin {
+ coinPub: string;
+ coinPriv: string;
+ contribution: AmountString;
+ denomPubHash: string;
+ denomSig: UnblindedSignature;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+}
+
+interface PeerCoinSelectionDetails {
exchangeBaseUrl: string;
/**
* Info of Coins that were selected.
*/
- coins: {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
- }[];
+ coins: SelectedPeerCoin[];
/**
* How much of the deposit fees is the customer paying?
@@ -195,152 +197,158 @@ export async function queryCoinInfosForSelection(
export async function selectPeerCoins(
ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- denominations: typeof WalletStoresV1.denominations;
- coins: typeof WalletStoresV1.coins;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- }>,
instructedAmount: AmountJson,
): Promise<SelectPeerCoinsResult> {
- const exchanges = await tx.exchanges.iter().toArray();
- const exchangeFeeGap: { [url: string]: AmountJson } = {};
- const currency = Amounts.currencyOf(instructedAmount);
- for (const exch of exchanges) {
- if (exch.detailsPointer?.currency !== currency) {
- continue;
- }
- const coins = (
- await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl)
- ).filter((x) => x.status === CoinStatus.Fresh);
- const coinInfos: CoinInfo[] = [];
- for (const coin of coins) {
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- if (!denom) {
- throw Error("denom not found");
- }
- coinInfos.push({
- coinPub: coin.coinPub,
- feeDeposit: Amounts.parseOrThrow(denom.feeDeposit),
- value: Amounts.parseOrThrow(denom.value),
- denomPubHash: denom.denomPubHash,
- coinPriv: coin.coinPriv,
- denomSig: coin.denomSig,
- maxAge: coin.maxAge,
- ageCommitmentProof: coin.ageCommitmentProof,
- });
- }
- if (coinInfos.length === 0) {
- continue;
- }
- coinInfos.sort(
- (o1, o2) =>
- -Amounts.cmp(o1.value, o2.value) ||
- strcmp(o1.denomPubHash, o2.denomPubHash),
- );
- let amountAcc = Amounts.zeroOfCurrency(currency);
- let depositFeesAcc = Amounts.zeroOfCurrency(currency);
- const resCoins: {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
- }[] = [];
- let lastDepositFee = Amounts.zeroOfCurrency(currency);
- for (const coin of coinInfos) {
- if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
- break;
+ return await ws.db
+ .mktx((x) => [
+ x.exchanges,
+ x.contractTerms,
+ x.coins,
+ x.coinAvailability,
+ x.denominations,
+ x.refreshGroups,
+ x.peerPushPaymentInitiations,
+ ])
+ .runReadWrite(async (tx) => {
+ const exchanges = await tx.exchanges.iter().toArray();
+ const exchangeFeeGap: { [url: string]: AmountJson } = {};
+ const currency = Amounts.currencyOf(instructedAmount);
+ for (const exch of exchanges) {
+ if (exch.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ const coins = (
+ await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl)
+ ).filter((x) => x.status === CoinStatus.Fresh);
+ const coinInfos: CoinInfo[] = [];
+ for (const coin of coins) {
+ const denom = await ws.getDenomInfo(
+ ws,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ if (!denom) {
+ throw Error("denom not found");
+ }
+ coinInfos.push({
+ coinPub: coin.coinPub,
+ feeDeposit: Amounts.parseOrThrow(denom.feeDeposit),
+ value: Amounts.parseOrThrow(denom.value),
+ denomPubHash: denom.denomPubHash,
+ coinPriv: coin.coinPriv,
+ denomSig: coin.denomSig,
+ maxAge: coin.maxAge,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ });
+ }
+ if (coinInfos.length === 0) {
+ continue;
+ }
+ coinInfos.sort(
+ (o1, o2) =>
+ -Amounts.cmp(o1.value, o2.value) ||
+ strcmp(o1.denomPubHash, o2.denomPubHash),
+ );
+ let amountAcc = Amounts.zeroOfCurrency(currency);
+ let depositFeesAcc = Amounts.zeroOfCurrency(currency);
+ const resCoins: {
+ coinPub: string;
+ coinPriv: string;
+ contribution: AmountString;
+ denomPubHash: string;
+ denomSig: UnblindedSignature;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+ }[] = [];
+ let lastDepositFee = Amounts.zeroOfCurrency(currency);
+ for (const coin of coinInfos) {
+ if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
+ break;
+ }
+ const gap = Amounts.add(
+ coin.feeDeposit,
+ Amounts.sub(instructedAmount, amountAcc).amount,
+ ).amount;
+ const contrib = Amounts.min(gap, coin.value);
+ amountAcc = Amounts.add(
+ amountAcc,
+ Amounts.sub(contrib, coin.feeDeposit).amount,
+ ).amount;
+ depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount;
+ resCoins.push({
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ contribution: Amounts.stringify(contrib),
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ });
+ lastDepositFee = coin.feeDeposit;
+ }
+ if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
+ const res: PeerCoinSelectionDetails = {
+ exchangeBaseUrl: exch.baseUrl,
+ coins: resCoins,
+ depositFees: depositFeesAcc,
+ };
+ return { type: "success", result: res };
+ }
+ const diff = Amounts.sub(instructedAmount, amountAcc).amount;
+ exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount;
+
+ continue;
}
- const gap = Amounts.add(
- coin.feeDeposit,
- Amounts.sub(instructedAmount, amountAcc).amount,
- ).amount;
- const contrib = Amounts.min(gap, coin.value);
- amountAcc = Amounts.add(
- amountAcc,
- Amounts.sub(contrib, coin.feeDeposit).amount,
- ).amount;
- depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount;
- resCoins.push({
- coinPriv: coin.coinPriv,
- coinPub: coin.coinPub,
- contribution: Amounts.stringify(contrib),
- denomPubHash: coin.denomPubHash,
- denomSig: coin.denomSig,
- ageCommitmentProof: coin.ageCommitmentProof,
+ // We were unable to select coins.
+ // Now we need to produce error details.
+
+ const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
+ currency,
});
- lastDepositFee = coin.feeDeposit;
- }
- if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
- const res: PeerCoinSelectionDetails = {
- exchangeBaseUrl: exch.baseUrl,
- coins: resCoins,
- depositFees: depositFeesAcc,
- };
- return { type: "success", result: res };
- }
- const diff = Amounts.sub(instructedAmount, amountAcc).amount;
- exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount;
- continue;
- }
- // We were unable to select coins.
- // Now we need to produce error details.
+ const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
- const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
- currency,
- });
+ for (const exch of exchanges) {
+ if (exch.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
+ currency,
+ restrictExchangeTo: exch.baseUrl,
+ });
+ let gap =
+ exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency);
+ if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) {
+ // Show fee gap only if we should've been able to pay with the material amount
+ gap = Amounts.zeroOfAmount(currency);
+ }
+ perExchange[exch.baseUrl] = {
+ balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
+ balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
+ feeGapEstimate: Amounts.stringify(gap),
+ };
+ }
- const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
+ const errDetails: PayPeerInsufficientBalanceDetails = {
+ amountRequested: Amounts.stringify(instructedAmount),
+ balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
+ balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
+ perExchange,
+ };
- for (const exch of exchanges) {
- if (exch.detailsPointer?.currency !== currency) {
- continue;
- }
- const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
- currency,
- restrictExchangeTo: exch.baseUrl,
+ return { type: "failure", insufficientBalanceDetails: errDetails };
});
- let gap = exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency);
- if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) {
- // Show fee gap only if we should've been able to pay with the material amount
- gap = Amounts.zeroOfAmount(currency);
- }
- perExchange[exch.baseUrl] = {
- balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
- balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
- feeGapEstimate: Amounts.stringify(gap),
- };
- }
-
- const errDetails: PayPeerInsufficientBalanceDetails = {
- amountRequested: Amounts.stringify(instructedAmount),
- balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
- balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
- perExchange,
- };
-
- return { type: "failure", insufficientBalanceDetails: errDetails };
}
export async function getTotalPeerPaymentCost(
ws: InternalWalletState,
- pcs: PeerCoinSelectionDetails,
+ pcs: SelectedPeerCoin[],
): Promise<AmountJson> {
return ws.db
.mktx((x) => [x.coins, x.denominations])
.runReadOnly(async (tx) => {
const costs: AmountJson[] = [];
- for (let i = 0; i < pcs.coins.length; i++) {
- const coin = await tx.coins.get(pcs.coins[i].coinPub);
+ for (let i = 0; i < pcs.length; i++) {
+ const coin = await tx.coins.get(pcs[i].coinPub);
if (!coin) {
throw Error("can't calculate payment cost, coin not found");
}
@@ -358,22 +366,22 @@ export async function getTotalPeerPaymentCost(
.filter((x) =>
Amounts.isSameCurrency(
DenominationRecord.getValue(x),
- pcs.coins[i].contribution,
+ pcs[i].contribution,
),
);
const amountLeft = Amounts.sub(
DenominationRecord.getValue(denom),
- pcs.coins[i].contribution,
+ pcs[i].contribution,
).amount;
const refreshCost = getTotalRefreshCost(
allDenoms,
DenominationRecord.toDenomInfo(denom),
amountLeft,
);
- costs.push(Amounts.parseOrThrow(pcs.coins[i].contribution));
+ costs.push(Amounts.parseOrThrow(pcs[i].contribution));
costs.push(refreshCost);
}
- const zero = Amounts.zeroOfAmount(pcs.coins[0].contribution);
+ const zero = Amounts.zeroOfAmount(pcs[0].contribution);
return Amounts.sum([zero, ...costs]).amount;
});
}
@@ -383,20 +391,7 @@ export async function preparePeerPushPayment(
req: PreparePeerPushPaymentRequest,
): Promise<PreparePeerPushPaymentResponse> {
const instructedAmount = Amounts.parseOrThrow(req.amount);
- const coinSelRes: SelectPeerCoinsResult = await ws.db
- .mktx((x) => [
- x.exchanges,
- x.contractTerms,
- x.coins,
- x.coinAvailability,
- x.denominations,
- x.refreshGroups,
- x.peerPushPaymentInitiations,
- ])
- .runReadWrite(async (tx) => {
- const selRes = await selectPeerCoins(ws, tx, instructedAmount);
- return selRes;
- });
+ const coinSelRes = await selectPeerCoins(ws, instructedAmount);
if (coinSelRes.type === "failure") {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
@@ -405,7 +400,10 @@ export async function preparePeerPushPayment(
},
);
}
- const totalAmount = await getTotalPeerPaymentCost(ws, coinSelRes.result);
+ const totalAmount = await getTotalPeerPaymentCost(
+ ws,
+ coinSelRes.result.coins,
+ );
return {
amountEffective: Amounts.stringify(totalAmount),
amountRaw: req.amount,
@@ -517,7 +515,28 @@ export async function initiatePeerPushPayment(
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({});
- const coinSelRes: SelectPeerCoinsResult = await ws.db
+
+ const coinSelRes = await selectPeerCoins(ws, instructedAmount);
+
+ if (coinSelRes.type !== "success") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ }
+
+ const sel = coinSelRes.result;
+
+ logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
+
+ const totalAmount = await getTotalPeerPaymentCost(
+ ws,
+ coinSelRes.result.coins,
+ );
+
+ await ws.db
.mktx((x) => [
x.exchanges,
x.contractTerms,
@@ -528,13 +547,6 @@ export async function initiatePeerPushPayment(
x.peerPushPaymentInitiations,
])
.runReadWrite(async (tx) => {
- const selRes = await selectPeerCoins(ws, tx, instructedAmount);
- if (selRes.type === "failure") {
- return selRes;
- }
-
- const sel = selRes.result;
-
await spendCoins(ws, tx, {
allocationId: `txn:peer-push-debit:${pursePair.pub}`,
coinPubs: sel.coins.map((x) => x.coinPub),
@@ -562,25 +574,14 @@ export async function initiatePeerPushPayment(
coinPubs: sel.coins.map((x) => x.coinPub),
contributions: sel.coins.map((x) => x.contribution),
},
+ totalCost: Amounts.stringify(totalAmount),
});
await tx.contractTerms.put({
h: hContractTerms,
contractTermsRaw: contractTerms,
});
-
- return selRes;
});
- logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
-
- if (coinSelRes.type !== "success") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
- },
- );
- }
await runOperationWithErrorReporting(
ws,
@@ -866,7 +867,22 @@ export async function acceptPeerPullPayment(
const instructedAmount = Amounts.parseOrThrow(
peerPullInc.contractTerms.amount,
);
- const coinSelRes: SelectPeerCoinsResult = await ws.db
+
+ const coinSelRes = await selectPeerCoins(ws, instructedAmount);
+ logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
+
+ if (coinSelRes.type !== "success") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ }
+
+ const sel = coinSelRes.result;
+
+ await ws.db
.mktx((x) => [
x.exchanges,
x.coins,
@@ -876,13 +892,6 @@ export async function acceptPeerPullPayment(
x.coinAvailability,
])
.runReadWrite(async (tx) => {
- const selRes = await selectPeerCoins(ws, tx, instructedAmount);
- if (selRes.type !== "success") {
- return selRes;
- }
-
- const sel = selRes.result;
-
await spendCoins(ws, tx, {
allocationId: `txn:peer-pull-debit:${req.peerPullPaymentIncomingId}`,
coinPubs: sel.coins.map((x) => x.coinPub),
@@ -900,19 +909,7 @@ export async function acceptPeerPullPayment(
}
pi.status = PeerPullPaymentIncomingStatus.Accepted;
await tx.peerPullPaymentIncoming.put(pi);
-
- return selRes;
});
- logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
-
- if (coinSelRes.type !== "success") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
- },
- );
- }
const pursePub = peerPullInc.pursePub;
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 43ee97239..80dc50eb8 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -346,7 +346,7 @@ function buildTransactionForPushPaymentDebit(
): Transaction {
return {
type: TransactionType.PeerPushDebit,
- amountEffective: pi.amount,
+ amountEffective: pi.totalCost,
amountRaw: pi.amount,
exchangeBaseUrl: pi.exchangeBaseUrl,
info: {