From b214934b75418d0d01c9556577d9594f1db5a319 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 21 Jun 2022 12:40:12 +0200 Subject: wallet-core: P2P push payments (still incomplete) --- .../src/operations/peer-to-peer.ts | 222 +++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 packages/taler-wallet-core/src/operations/peer-to-peer.ts (limited to 'packages/taler-wallet-core/src/operations') diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts new file mode 100644 index 000000000..e2ae1e66e --- /dev/null +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -0,0 +1,222 @@ +/* + 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"); +} -- cgit v1.2.3