wallet-core: store total p2p push cost in DB
This commit is contained in:
parent
a3f9e86805
commit
a31b8c3c31
@ -1713,6 +1713,8 @@ export interface PeerPushPaymentInitiationRecord {
|
||||
*/
|
||||
amount: AmountString;
|
||||
|
||||
totalCost: AmountString;
|
||||
|
||||
coinSel: PeerPushPaymentCoinSelection;
|
||||
|
||||
contractTermsHash: HashCodeString;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -346,7 +346,7 @@ function buildTransactionForPushPaymentDebit(
|
||||
): Transaction {
|
||||
return {
|
||||
type: TransactionType.PeerPushDebit,
|
||||
amountEffective: pi.amount,
|
||||
amountEffective: pi.totalCost,
|
||||
amountRaw: pi.amount,
|
||||
exchangeBaseUrl: pi.exchangeBaseUrl,
|
||||
info: {
|
||||
|
Loading…
Reference in New Issue
Block a user