196 lines
6.0 KiB
TypeScript
196 lines
6.0 KiB
TypeScript
/*
|
|
This file is part of GNU Taler
|
|
(C) 2022 GNUnet e.V.
|
|
|
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
terms of the GNU General Public License as published by the Free Software
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
/**
|
|
* Imports.
|
|
*/
|
|
import {
|
|
AgeCommitmentProof,
|
|
AmountJson,
|
|
AmountString,
|
|
Amounts,
|
|
Codec,
|
|
CoinPublicKeyString,
|
|
CoinStatus,
|
|
HttpStatusCode,
|
|
Logger,
|
|
NotificationType,
|
|
PayPeerInsufficientBalanceDetails,
|
|
TalerError,
|
|
TalerErrorCode,
|
|
TalerProtocolTimestamp,
|
|
UnblindedSignature,
|
|
buildCodecForObject,
|
|
codecForAmountString,
|
|
codecForTimestamp,
|
|
codecOptional,
|
|
j2s,
|
|
strcmp,
|
|
} from "@gnu-taler/taler-util";
|
|
import { SpendCoinDetails } from "../crypto/cryptoImplementation.js";
|
|
import {
|
|
DenominationRecord,
|
|
PeerPushPaymentCoinSelection,
|
|
ReserveRecord,
|
|
} from "../db.js";
|
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
|
import { checkDbInvariant } from "../util/invariants.js";
|
|
import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
|
|
import { getTotalRefreshCost } from "./refresh.js";
|
|
import type { PeerCoinInfo, PeerCoinSelectionRequest, SelectPeerCoinsResult, SelectedPeerCoin } from "../util/coinSelection.js";
|
|
|
|
const logger = new Logger("operations/peer-to-peer.ts");
|
|
|
|
/**
|
|
* Get information about the coin selected for signatures
|
|
*
|
|
* @param ws
|
|
* @param csel
|
|
* @returns
|
|
*/
|
|
export async function queryCoinInfosForSelection(
|
|
ws: InternalWalletState,
|
|
csel: PeerPushPaymentCoinSelection,
|
|
): Promise<SpendCoinDetails[]> {
|
|
let infos: SpendCoinDetails[] = [];
|
|
await ws.db
|
|
.mktx((x) => [x.coins, x.denominations])
|
|
.runReadOnly(async (tx) => {
|
|
for (let i = 0; i < csel.coinPubs.length; i++) {
|
|
const coin = await tx.coins.get(csel.coinPubs[i]);
|
|
if (!coin) {
|
|
throw Error("coin not found anymore");
|
|
}
|
|
const denom = await ws.getDenomInfo(
|
|
ws,
|
|
tx,
|
|
coin.exchangeBaseUrl,
|
|
coin.denomPubHash,
|
|
);
|
|
if (!denom) {
|
|
throw Error("denom for coin not found anymore");
|
|
}
|
|
infos.push({
|
|
coinPriv: coin.coinPriv,
|
|
coinPub: coin.coinPub,
|
|
denomPubHash: coin.denomPubHash,
|
|
denomSig: coin.denomSig,
|
|
ageCommitmentProof: coin.ageCommitmentProof,
|
|
contribution: csel.contributions[i],
|
|
});
|
|
}
|
|
});
|
|
return infos;
|
|
}
|
|
|
|
|
|
|
|
export async function getTotalPeerPaymentCost(
|
|
ws: InternalWalletState,
|
|
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.length; i++) {
|
|
const coin = await tx.coins.get(pcs[i].coinPub);
|
|
if (!coin) {
|
|
throw Error("can't calculate payment cost, coin not found");
|
|
}
|
|
const denom = await tx.denominations.get([
|
|
coin.exchangeBaseUrl,
|
|
coin.denomPubHash,
|
|
]);
|
|
if (!denom) {
|
|
throw Error(
|
|
"can't calculate payment cost, denomination for coin not found",
|
|
);
|
|
}
|
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
|
.iter(coin.exchangeBaseUrl)
|
|
.filter((x) =>
|
|
Amounts.isSameCurrency(
|
|
DenominationRecord.getValue(x),
|
|
pcs[i].contribution,
|
|
),
|
|
);
|
|
const amountLeft = Amounts.sub(
|
|
DenominationRecord.getValue(denom),
|
|
pcs[i].contribution,
|
|
).amount;
|
|
const refreshCost = getTotalRefreshCost(
|
|
allDenoms,
|
|
DenominationRecord.toDenomInfo(denom),
|
|
amountLeft,
|
|
ws.config.testing.denomselAllowLate,
|
|
);
|
|
costs.push(Amounts.parseOrThrow(pcs[i].contribution));
|
|
costs.push(refreshCost);
|
|
}
|
|
const zero = Amounts.zeroOfAmount(pcs[0].contribution);
|
|
return Amounts.sum([zero, ...costs]).amount;
|
|
});
|
|
}
|
|
|
|
interface ExchangePurseStatus {
|
|
balance: AmountString;
|
|
deposit_timestamp?: TalerProtocolTimestamp;
|
|
merge_timestamp?: TalerProtocolTimestamp;
|
|
}
|
|
|
|
export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
|
|
buildCodecForObject<ExchangePurseStatus>()
|
|
.property("balance", codecForAmountString())
|
|
.property("deposit_timestamp", codecOptional(codecForTimestamp))
|
|
.property("merge_timestamp", codecOptional(codecForTimestamp))
|
|
.build("ExchangePurseStatus");
|
|
|
|
export async function getMergeReserveInfo(
|
|
ws: InternalWalletState,
|
|
req: {
|
|
exchangeBaseUrl: string;
|
|
},
|
|
): Promise<ReserveRecord> {
|
|
// We have to eagerly create the key pair outside of the transaction,
|
|
// due to the async crypto API.
|
|
const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
|
|
|
|
const mergeReserveRecord: ReserveRecord = await ws.db
|
|
.mktx((x) => [x.exchanges, x.reserves, x.withdrawalGroups])
|
|
.runReadWrite(async (tx) => {
|
|
const ex = await tx.exchanges.get(req.exchangeBaseUrl);
|
|
checkDbInvariant(!!ex);
|
|
if (ex.currentMergeReserveRowId != null) {
|
|
const reserve = await tx.reserves.get(ex.currentMergeReserveRowId);
|
|
checkDbInvariant(!!reserve);
|
|
return reserve;
|
|
}
|
|
const reserve: ReserveRecord = {
|
|
reservePriv: newReservePair.priv,
|
|
reservePub: newReservePair.pub,
|
|
};
|
|
const insertResp = await tx.reserves.put(reserve);
|
|
checkDbInvariant(typeof insertResp.key === "number");
|
|
reserve.rowId = insertResp.key;
|
|
ex.currentMergeReserveRowId = reserve.rowId;
|
|
await tx.exchanges.put(ex);
|
|
return reserve;
|
|
});
|
|
|
|
return mergeReserveRecord;
|
|
}
|