aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-06-21 12:40:12 +0200
committerFlorian Dold <florian@dold.me>2022-07-08 11:08:30 +0200
commitb214934b75418d0d01c9556577d9594f1db5a319 (patch)
treed8ec18217a16e6b89859b30003a4a825fc63a66e /packages/taler-wallet-core/src/operations
parent05cdbfb534bb194dbe6bdf049113ebea8139234f (diff)
downloadwallet-core-b214934b75418d0d01c9556577d9594f1db5a319.tar.xz
wallet-core: P2P push payments (still incomplete)
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
-rw-r--r--packages/taler-wallet-core/src/operations/peer-to-peer.ts222
1 files changed, 222 insertions, 0 deletions
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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * 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<InitiatePeerPushPaymentResponse> {
+ 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");
+}