aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/pay-peer.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-10-08 20:56:57 +0200
committerFlorian Dold <florian@dold.me>2022-10-08 23:07:07 +0200
commit526f4eba9554f27e33afb0e02d19d870825b038c (patch)
treec35e41a20a3bc90da3beb81fa7831505ee64cfee /packages/taler-wallet-core/src/operations/pay-peer.ts
parenteace0e0e7aad9113af758b829fffd873826e36e3 (diff)
downloadwallet-core-526f4eba9554f27e33afb0e02d19d870825b038c.tar.xz
wallet-core: Clean up merchant payments DB schema
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer.ts847
1 files changed, 847 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts
new file mode 100644
index 000000000..e9185a9d4
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pay-peer.ts
@@ -0,0 +1,847 @@
+/*
+ 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 {
+ AbsoluteTime,
+ AcceptPeerPullPaymentRequest,
+ AcceptPeerPullPaymentResponse,
+ AcceptPeerPushPaymentRequest,
+ AcceptPeerPushPaymentResponse,
+ AgeCommitmentProof,
+ AmountJson,
+ Amounts,
+ AmountString,
+ buildCodecForObject,
+ CheckPeerPullPaymentRequest,
+ CheckPeerPullPaymentResponse,
+ CheckPeerPushPaymentRequest,
+ CheckPeerPushPaymentResponse,
+ Codec,
+ codecForAmountString,
+ codecForAny,
+ codecForExchangeGetContractResponse,
+ constructPayPullUri,
+ constructPayPushUri,
+ ContractTermsUtil,
+ decodeCrock,
+ Duration,
+ eddsaGetPublic,
+ encodeCrock,
+ ExchangePurseDeposits,
+ ExchangePurseMergeRequest,
+ ExchangeReservePurseRequest,
+ getRandomBytes,
+ InitiatePeerPullPaymentRequest,
+ InitiatePeerPullPaymentResponse,
+ InitiatePeerPushPaymentRequest,
+ InitiatePeerPushPaymentResponse,
+ j2s,
+ Logger,
+ parsePayPullUri,
+ parsePayPushUri,
+ RefreshReason,
+ strcmp,
+ TalerProtocolTimestamp,
+ TransactionType,
+ UnblindedSignature,
+ WalletAccountMergeFlags,
+} from "@gnu-taler/taler-util";
+import {
+ CoinStatus,
+ MergeReserveInfo,
+ WithdrawalGroupStatus,
+ WalletStoresV1,
+ WithdrawalRecordType,
+} from "../db.js";
+import { InternalWalletState } from "../internal-wallet-state.js";
+import { readSuccessResponseJsonOrThrow } from "../util/http.js";
+import { checkDbInvariant } from "../util/invariants.js";
+import { GetReadOnlyAccess } from "../util/query.js";
+import { spendCoins, makeEventId } from "../operations/common.js";
+import { updateExchangeFromUrl } from "./exchanges.js";
+import { internalCreateWithdrawalGroup } from "./withdraw.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;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+ }[];
+
+ /**
+ * 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;
+
+ maxAge: number;
+ ageCommitmentProof?: AgeCommitmentProof;
+}
+
+export async function selectPeerCoins(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<{
+ exchanges: typeof WalletStoresV1.exchanges;
+ denominations: typeof WalletStoresV1.denominations;
+ coins: typeof WalletStoresV1.coins;
+ }>,
+ instructedAmount: AmountJson,
+): Promise<PeerCoinSelection | undefined> {
+ 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,
+ 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.getZero(instructedAmount.currency);
+ let depositFeesAcc = Amounts.getZero(instructedAmount.currency);
+ const resCoins: {
+ coinPub: string;
+ coinPriv: string;
+ contribution: AmountString;
+ denomPubHash: string;
+ denomSig: UnblindedSignature;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+ }[] = [];
+ 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,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ });
+ }
+ continue;
+ }
+ return undefined;
+}
+
+export async function initiatePeerToPeerPush(
+ ws: InternalWalletState,
+ req: InitiatePeerPushPaymentRequest,
+): Promise<InitiatePeerPushPaymentResponse> {
+ const instructedAmount = Amounts.parseOrThrow(req.amount);
+
+ const pursePair = await ws.cryptoApi.createEddsaKeypair({});
+ const mergePair = await ws.cryptoApi.createEddsaKeypair({});
+
+ const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const contractTerms = {
+ ...req.partialContractTerms,
+ purse_expiration: purseExpiration,
+ amount: req.amount,
+ };
+
+ const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
+
+ const econtractResp = await ws.cryptoApi.encryptContractForMerge({
+ contractTerms,
+ mergePriv: mergePair.priv,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ });
+
+ const coinSelRes: PeerCoinSelection | undefined = await ws.db
+ .mktx((x) => [
+ x.exchanges,
+ x.coins,
+ x.coinAvailability,
+ x.denominations,
+ x.refreshGroups,
+ x.peerPullPaymentInitiations,
+ x.peerPushPaymentInitiations,
+ ])
+ .runReadWrite(async (tx) => {
+ const sel = await selectPeerCoins(ws, tx, instructedAmount);
+ if (!sel) {
+ return undefined;
+ }
+
+ await spendCoins(ws, tx, {
+ allocationId: `peer-push:${pursePair.pub}`,
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPush,
+ });
+
+ await tx.peerPushPaymentInitiations.add({
+ amount: Amounts.stringify(instructedAmount),
+ contractPriv: econtractResp.contractPriv,
+ contractTerms,
+ exchangeBaseUrl: sel.exchangeBaseUrl,
+ mergePriv: mergePair.priv,
+ mergePub: mergePair.pub,
+ // FIXME: only set this later!
+ purseCreated: true,
+ purseExpiration: purseExpiration,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ timestampCreated: TalerProtocolTimestamp.now(),
+ });
+
+ return sel;
+ });
+ logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
+
+ if (!coinSelRes) {
+ throw Error("insufficient balance");
+ }
+
+ 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,
+ econtract: econtractResp.econtract,
+ });
+
+ const resp = await httpResp.json();
+
+ logger.info(`resp: ${j2s(resp)}`);
+
+ if (httpResp.status !== 200) {
+ throw Error("got error response from exchange");
+ }
+
+ return {
+ contractPriv: econtractResp.contractPriv,
+ mergePriv: mergePair.priv,
+ pursePub: pursePair.pub,
+ exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
+ talerUri: constructPayPushUri({
+ exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
+ contractPriv: econtractResp.contractPriv,
+ }),
+ transactionId: makeEventId(TransactionType.PeerPushDebit, pursePair.pub),
+ };
+}
+
+interface ExchangePurseStatus {
+ balance: AmountString;
+}
+
+export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
+ buildCodecForObject<ExchangePurseStatus>()
+ .property("balance", codecForAmountString())
+ .build("ExchangePurseStatus");
+
+export async function checkPeerPushPayment(
+ ws: InternalWalletState,
+ req: CheckPeerPushPaymentRequest,
+): Promise<CheckPeerPushPaymentResponse> {
+ // FIXME: Check if existing record exists!
+
+ const uri = parsePayPushUri(req.talerUri);
+
+ if (!uri) {
+ throw Error("got invalid taler://pay-push URI");
+ }
+
+ const exchangeBaseUrl = uri.exchangeBaseUrl;
+
+ await updateExchangeFromUrl(ws, exchangeBaseUrl);
+
+ const contractPriv = uri.contractPriv;
+ const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
+
+ const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
+
+ const contractHttpResp = await ws.http.get(getContractUrl.href);
+
+ const contractResp = await readSuccessResponseJsonOrThrow(
+ contractHttpResp,
+ codecForExchangeGetContractResponse(),
+ );
+
+ const pursePub = contractResp.purse_pub;
+
+ const dec = await ws.cryptoApi.decryptContractForMerge({
+ ciphertext: contractResp.econtract,
+ contractPriv: contractPriv,
+ pursePub: pursePub,
+ });
+
+ const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);
+
+ const purseHttpResp = await ws.http.get(getPurseUrl.href);
+
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ purseHttpResp,
+ codecForExchangePurseStatus(),
+ );
+
+ const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32));
+
+ await ws.db
+ .mktx((x) => [x.peerPushPaymentIncoming])
+ .runReadWrite(async (tx) => {
+ await tx.peerPushPaymentIncoming.add({
+ peerPushPaymentIncomingId,
+ contractPriv: contractPriv,
+ exchangeBaseUrl: exchangeBaseUrl,
+ mergePriv: dec.mergePriv,
+ pursePub: pursePub,
+ timestamp: TalerProtocolTimestamp.now(),
+ contractTerms: dec.contractTerms,
+ });
+ });
+
+ return {
+ amount: purseStatus.balance,
+ contractTerms: dec.contractTerms,
+ peerPushPaymentIncomingId,
+ };
+}
+
+export function talerPaytoFromExchangeReserve(
+ exchangeBaseUrl: string,
+ reservePub: string,
+): string {
+ const url = new URL(exchangeBaseUrl);
+ let proto: string;
+ if (url.protocol === "http:") {
+ proto = "taler-reserve-http";
+ } else if (url.protocol === "https:") {
+ proto = "taler-reserve";
+ } else {
+ throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
+ }
+
+ let path = url.pathname;
+ if (!path.endsWith("/")) {
+ path = path + "/";
+ }
+
+ return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
+}
+
+async function getMergeReserveInfo(
+ ws: InternalWalletState,
+ req: {
+ exchangeBaseUrl: string;
+ },
+): Promise<MergeReserveInfo> {
+ // 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 mergeReserveInfo: MergeReserveInfo = await ws.db
+ .mktx((x) => [x.exchanges, x.withdrawalGroups])
+ .runReadWrite(async (tx) => {
+ const ex = await tx.exchanges.get(req.exchangeBaseUrl);
+ checkDbInvariant(!!ex);
+ if (ex.currentMergeReserveInfo) {
+ return ex.currentMergeReserveInfo;
+ }
+ await tx.exchanges.put(ex);
+ ex.currentMergeReserveInfo = {
+ reservePriv: newReservePair.priv,
+ reservePub: newReservePair.pub,
+ };
+ return ex.currentMergeReserveInfo;
+ });
+
+ return mergeReserveInfo;
+}
+
+export async function acceptPeerPushPayment(
+ ws: InternalWalletState,
+ req: AcceptPeerPushPaymentRequest,
+): Promise<AcceptPeerPushPaymentResponse> {
+ const peerInc = await ws.db
+ .mktx((x) => [x.peerPushPaymentIncoming])
+ .runReadOnly(async (tx) => {
+ return tx.peerPushPaymentIncoming.get(req.peerPushPaymentIncomingId);
+ });
+
+ if (!peerInc) {
+ throw Error(
+ `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`,
+ );
+ }
+
+ await updateExchangeFromUrl(ws, peerInc.exchangeBaseUrl);
+
+ const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
+
+ const mergeReserveInfo = await getMergeReserveInfo(ws, {
+ exchangeBaseUrl: peerInc.exchangeBaseUrl,
+ });
+
+ const mergeTimestamp = TalerProtocolTimestamp.now();
+
+ const reservePayto = talerPaytoFromExchangeReserve(
+ peerInc.exchangeBaseUrl,
+ mergeReserveInfo.reservePub,
+ );
+
+ const sigRes = await ws.cryptoApi.signPurseMerge({
+ contractTermsHash: ContractTermsUtil.hashContractTerms(
+ peerInc.contractTerms,
+ ),
+ flags: WalletAccountMergeFlags.MergeFullyPaidPurse,
+ mergePriv: peerInc.mergePriv,
+ mergeTimestamp: mergeTimestamp,
+ purseAmount: Amounts.stringify(amount),
+ purseExpiration: peerInc.contractTerms.purse_expiration,
+ purseFee: Amounts.stringify(Amounts.getZero(amount.currency)),
+ pursePub: peerInc.pursePub,
+ reservePayto,
+ reservePriv: mergeReserveInfo.reservePriv,
+ });
+
+ const mergePurseUrl = new URL(
+ `purses/${peerInc.pursePub}/merge`,
+ peerInc.exchangeBaseUrl,
+ );
+
+ const mergeReq: ExchangePurseMergeRequest = {
+ payto_uri: reservePayto,
+ merge_timestamp: mergeTimestamp,
+ merge_sig: sigRes.mergeSig,
+ reserve_sig: sigRes.accountSig,
+ };
+
+ const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
+
+ logger.info(`merge request: ${j2s(mergeReq)}`);
+ const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
+ logger.info(`merge response: ${j2s(res)}`);
+
+ const wg = await internalCreateWithdrawalGroup(ws, {
+ amount,
+ wgInfo: {
+ withdrawalType: WithdrawalRecordType.PeerPushCredit,
+ contractTerms: peerInc.contractTerms,
+ },
+ exchangeBaseUrl: peerInc.exchangeBaseUrl,
+ reserveStatus: WithdrawalGroupStatus.QueryingStatus,
+ reserveKeyPair: {
+ priv: mergeReserveInfo.reservePriv,
+ pub: mergeReserveInfo.reservePub,
+ },
+ });
+
+ return {
+ transactionId: makeEventId(
+ TransactionType.PeerPushCredit,
+ wg.withdrawalGroupId,
+ ),
+ };
+}
+
+/**
+ * FIXME: Bad name!
+ */
+export async function acceptPeerPullPayment(
+ ws: InternalWalletState,
+ req: AcceptPeerPullPaymentRequest,
+): Promise<AcceptPeerPullPaymentResponse> {
+ const peerPullInc = await ws.db
+ .mktx((x) => [x.peerPullPaymentIncoming])
+ .runReadOnly(async (tx) => {
+ return tx.peerPullPaymentIncoming.get(req.peerPullPaymentIncomingId);
+ });
+
+ if (!peerPullInc) {
+ throw Error(
+ `can't accept unknown incoming p2p pull payment (${req.peerPullPaymentIncomingId})`,
+ );
+ }
+
+ const instructedAmount = Amounts.parseOrThrow(
+ peerPullInc.contractTerms.amount,
+ );
+ const coinSelRes: PeerCoinSelection | undefined = await ws.db
+ .mktx((x) => [
+ x.exchanges,
+ x.coins,
+ x.denominations,
+ x.refreshGroups,
+ x.peerPullPaymentIncoming,
+ x.coinAvailability,
+ ])
+ .runReadWrite(async (tx) => {
+ const sel = await selectPeerCoins(ws, tx, instructedAmount);
+ if (!sel) {
+ return undefined;
+ }
+
+ await spendCoins(ws, tx, {
+ allocationId: `peer-pull:${req.peerPullPaymentIncomingId}`,
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPull,
+ });
+
+ const pi = await tx.peerPullPaymentIncoming.get(
+ req.peerPullPaymentIncomingId,
+ );
+ if (!pi) {
+ throw Error();
+ }
+ pi.accepted = true;
+ await tx.peerPullPaymentIncoming.put(pi);
+
+ return sel;
+ });
+ logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
+
+ if (!coinSelRes) {
+ throw Error("insufficient balance");
+ }
+
+ const pursePub = peerPullInc.pursePub;
+
+ const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
+ exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
+ pursePub,
+ coins: coinSelRes.coins,
+ });
+
+ const purseDepositUrl = new URL(
+ `purses/${pursePub}/deposit`,
+ coinSelRes.exchangeBaseUrl,
+ );
+
+ const depositPayload: ExchangePurseDeposits = {
+ deposits: depositSigsResp.deposits,
+ };
+
+ const httpResp = await ws.http.postJson(purseDepositUrl.href, depositPayload);
+ const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
+ logger.trace(`purse deposit response: ${j2s(resp)}`);
+
+ return {
+ transactionId: makeEventId(
+ TransactionType.PeerPullDebit,
+ req.peerPullPaymentIncomingId,
+ ),
+ };
+}
+
+export async function checkPeerPullPayment(
+ ws: InternalWalletState,
+ req: CheckPeerPullPaymentRequest,
+): Promise<CheckPeerPullPaymentResponse> {
+ const uri = parsePayPullUri(req.talerUri);
+
+ if (!uri) {
+ throw Error("got invalid taler://pay-push URI");
+ }
+
+ const exchangeBaseUrl = uri.exchangeBaseUrl;
+ const contractPriv = uri.contractPriv;
+ const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
+
+ const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
+
+ const contractHttpResp = await ws.http.get(getContractUrl.href);
+
+ const contractResp = await readSuccessResponseJsonOrThrow(
+ contractHttpResp,
+ codecForExchangeGetContractResponse(),
+ );
+
+ const pursePub = contractResp.purse_pub;
+
+ const dec = await ws.cryptoApi.decryptContractForDeposit({
+ ciphertext: contractResp.econtract,
+ contractPriv: contractPriv,
+ pursePub: pursePub,
+ });
+
+ const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl);
+
+ const purseHttpResp = await ws.http.get(getPurseUrl.href);
+
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ purseHttpResp,
+ codecForExchangePurseStatus(),
+ );
+
+ const peerPullPaymentIncomingId = encodeCrock(getRandomBytes(32));
+
+ await ws.db
+ .mktx((x) => [x.peerPullPaymentIncoming])
+ .runReadWrite(async (tx) => {
+ await tx.peerPullPaymentIncoming.add({
+ peerPullPaymentIncomingId,
+ contractPriv: contractPriv,
+ exchangeBaseUrl: exchangeBaseUrl,
+ pursePub: pursePub,
+ timestampCreated: TalerProtocolTimestamp.now(),
+ contractTerms: dec.contractTerms,
+ paid: false,
+ accepted: false,
+ });
+ });
+
+ return {
+ amount: purseStatus.balance,
+ contractTerms: dec.contractTerms,
+ peerPullPaymentIncomingId,
+ };
+}
+
+/**
+ * Initiate a peer pull payment.
+ */
+export async function initiatePeerRequestForPay(
+ ws: InternalWalletState,
+ req: InitiatePeerPullPaymentRequest,
+): Promise<InitiatePeerPullPaymentResponse> {
+ await updateExchangeFromUrl(ws, req.exchangeBaseUrl);
+
+ const mergeReserveInfo = await getMergeReserveInfo(ws, {
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ });
+
+ const mergeTimestamp = TalerProtocolTimestamp.now();
+
+ const pursePair = await ws.cryptoApi.createEddsaKeypair({});
+ const mergePair = await ws.cryptoApi.createEddsaKeypair({});
+
+ const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const reservePayto = talerPaytoFromExchangeReserve(
+ req.exchangeBaseUrl,
+ mergeReserveInfo.reservePub,
+ );
+
+ const contractTerms = {
+ ...req.partialContractTerms,
+ amount: req.amount,
+ purse_expiration: purseExpiration,
+ };
+
+ const econtractResp = await ws.cryptoApi.encryptContractForDeposit({
+ contractTerms,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ });
+
+ const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
+
+ const purseFee = Amounts.stringify(
+ Amounts.getZero(Amounts.parseOrThrow(req.amount).currency),
+ );
+
+ const sigRes = await ws.cryptoApi.signReservePurseCreate({
+ contractTermsHash: hContractTerms,
+ flags: WalletAccountMergeFlags.CreateWithPurseFee,
+ mergePriv: mergePair.priv,
+ mergeTimestamp: mergeTimestamp,
+ purseAmount: req.amount,
+ purseExpiration: purseExpiration,
+ purseFee: purseFee,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ reservePayto,
+ reservePriv: mergeReserveInfo.reservePriv,
+ });
+
+ await ws.db
+ .mktx((x) => [x.peerPullPaymentInitiations])
+ .runReadWrite(async (tx) => {
+ await tx.peerPullPaymentInitiations.put({
+ amount: req.amount,
+ contractTerms,
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ });
+ });
+
+ const reservePurseReqBody: ExchangeReservePurseRequest = {
+ merge_sig: sigRes.mergeSig,
+ merge_timestamp: mergeTimestamp,
+ h_contract_terms: hContractTerms,
+ merge_pub: mergePair.pub,
+ min_age: 0,
+ purse_expiration: purseExpiration,
+ purse_fee: purseFee,
+ purse_pub: pursePair.pub,
+ purse_sig: sigRes.purseSig,
+ purse_value: req.amount,
+ reserve_sig: sigRes.accountSig,
+ econtract: econtractResp.econtract,
+ };
+
+ logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
+
+ const reservePurseMergeUrl = new URL(
+ `reserves/${mergeReserveInfo.reservePub}/purse`,
+ req.exchangeBaseUrl,
+ );
+
+ const httpResp = await ws.http.postJson(
+ reservePurseMergeUrl.href,
+ reservePurseReqBody,
+ );
+
+ const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
+
+ logger.info(`reserve merge response: ${j2s(resp)}`);
+
+ const wg = await internalCreateWithdrawalGroup(ws, {
+ amount: Amounts.parseOrThrow(req.amount),
+ wgInfo: {
+ withdrawalType: WithdrawalRecordType.PeerPullCredit,
+ contractTerms,
+ contractPriv: econtractResp.contractPriv,
+ },
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ reserveStatus: WithdrawalGroupStatus.QueryingStatus,
+ reserveKeyPair: {
+ priv: mergeReserveInfo.reservePriv,
+ pub: mergeReserveInfo.reservePub,
+ },
+ });
+
+ return {
+ talerUri: constructPayPullUri({
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ contractPriv: econtractResp.contractPriv,
+ }),
+ transactionId: makeEventId(
+ TransactionType.PeerPullCredit,
+ wg.withdrawalGroupId,
+ ),
+ };
+}