/* This file is part of GNU Taler (C) 2019 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 */ /** * Imports. */ import { AmountJson, Amounts, Logger, InitiatePeerPushPaymentResponse, InitiatePeerPushPaymentRequest, strcmp, CoinPublicKeyString, j2s, getRandomBytes, Duration, durationAdd, TalerProtocolTimestamp, AbsoluteTime, encodeCrock, AmountString, UnblindedSignature, } from "@gnu-taler/taler-util"; import { CoinStatus } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; const logger = new Logger("operations/peer-to-peer.ts"); export interface PeerCoinSelection { exchangeBaseUrl: string; /** * Info of Coins that were selected. */ coins: { coinPub: string; coinPriv: string; contribution: AmountString; denomPubHash: string; denomSig: UnblindedSignature; }[]; /** * How much of the deposit fees is the customer paying? */ depositFees: AmountJson; } interface CoinInfo { /** * Public key of the coin. */ coinPub: string; coinPriv: string; /** * Deposit fee for the coin. */ feeDeposit: AmountJson; value: AmountJson; denomPubHash: string; denomSig: UnblindedSignature; } export async function initiatePeerToPeerPush( ws: InternalWalletState, req: InitiatePeerPushPaymentRequest, ): Promise { const instructedAmount = Amounts.parseOrThrow(req.amount); const coinSelRes: PeerCoinSelection | undefined = await ws.db .mktx((x) => ({ exchanges: x.exchanges, coins: x.coins, denominations: x.denominations, })) .runReadOnly(async (tx) => { const exchanges = await tx.exchanges.iter().toArray(); for (const exch of exchanges) { if (exch.detailsPointer?.currency !== instructedAmount.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: denom.feeDeposit, value: denom.value, denomPubHash: denom.denomPubHash, coinPriv: coin.coinPriv, denomSig: coin.denomSig, }); } if (coinInfos.length === 0) { continue; } coinInfos.sort( (o1, o2) => -Amounts.cmp(o1.value, o2.value) || strcmp(o1.denomPubHash, o2.denomPubHash), ); let amountAcc = Amounts.getZero(instructedAmount.currency); let depositFeesAcc = Amounts.getZero(instructedAmount.currency); const resCoins: { coinPub: string; coinPriv: string; contribution: AmountString; denomPubHash: string; denomSig: UnblindedSignature; }[] = []; for (const coin of coinInfos) { if (Amounts.cmp(amountAcc, instructedAmount) >= 0) { const res: PeerCoinSelection = { exchangeBaseUrl: exch.baseUrl, coins: resCoins, depositFees: depositFeesAcc, }; return res; } 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, }); } continue; } return undefined; }); logger.info(`selected p2p coins: ${j2s(coinSelRes)}`); if (!coinSelRes) { throw Error("insufficient balance"); } const pursePair = await ws.cryptoApi.createEddsaKeypair({}); const mergePair = await ws.cryptoApi.createEddsaKeypair({}); const hContractTerms = encodeCrock(getRandomBytes(64)); const purseExpiration = AbsoluteTime.toTimestamp( AbsoluteTime.addDuration( AbsoluteTime.now(), Duration.fromSpec({ days: 2 }), ), ); const purseSigResp = await ws.cryptoApi.signPurseCreation({ hContractTerms, mergePub: mergePair.pub, minAge: 0, purseAmount: Amounts.stringify(instructedAmount), purseExpiration, pursePriv: pursePair.priv, }); const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ exchangeBaseUrl: coinSelRes.exchangeBaseUrl, pursePub: pursePair.pub, coins: coinSelRes.coins, }); const createPurseUrl = new URL( `purses/${pursePair.pub}/create`, coinSelRes.exchangeBaseUrl, ); const httpResp = await ws.http.postJson(createPurseUrl.href, { amount: Amounts.stringify(instructedAmount), merge_pub: mergePair.pub, purse_sig: purseSigResp.sig, h_contract_terms: hContractTerms, purse_expiration: purseExpiration, deposits: depositSigsResp.deposits, min_age: 0, }); const resp = await httpResp.json(); logger.info(`resp: ${j2s(resp)}`); throw Error("not yet implemented"); }