From a31b8c3c3105d0ba11f2a1c513c6b6bec3ebeb49 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 13 Jan 2023 02:24:19 +0100 Subject: wallet-core: store total p2p push cost in DB --- packages/taler-wallet-core/src/db.ts | 2 + .../taler-wallet-core/src/operations/pay-peer.ts | 389 ++++++++++----------- .../src/operations/transactions.ts | 2 +- 3 files changed, 196 insertions(+), 197 deletions(-) (limited to 'packages') 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 { - 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 { 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 { 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: { -- cgit v1.2.3