wallet-core: store total p2p push cost in DB

This commit is contained in:
Florian Dold 2023-01-13 02:24:19 +01:00
parent a3f9e86805
commit a31b8c3c31
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 197 additions and 198 deletions

View File

@ -1713,6 +1713,8 @@ export interface PeerPushPaymentInitiationRecord {
*/
amount: AmountString;
totalCost: AmountString;
coinSel: PeerPushPaymentCoinSelection;
contractTermsHash: HashCodeString;

View File

@ -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");
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;
}
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,
// We were unable to select coins.
// Now we need to produce error details.
const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
currency,
});
}
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 perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
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 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,
const errDetails: PayPeerInsufficientBalanceDetails = {
amountRequested: Amounts.stringify(instructedAmount),
balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
perExchange,
};
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 infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
currency,
});
const perExchange: PayPeerInsufficientBalanceDetails["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;

View File

@ -346,7 +346,7 @@ function buildTransactionForPushPaymentDebit(
): Transaction {
return {
type: TransactionType.PeerPushDebit,
amountEffective: pi.amount,
amountEffective: pi.totalCost,
amountRaw: pi.amount,
exchangeBaseUrl: pi.exchangeBaseUrl,
info: {