wallet-core: restructure p2p impl
This commit is contained in:
parent
f3d4ff4e3a
commit
fda5a0ed87
463
packages/taler-wallet-core/src/operations/pay-peer-common.ts
Normal file
463
packages/taler-wallet-core/src/operations/pay-peer-common.ts
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
/*
|
||||||
|
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,
|
||||||
|
Logger,
|
||||||
|
PayPeerInsufficientBalanceDetails,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
|
UnblindedSignature,
|
||||||
|
buildCodecForObject,
|
||||||
|
codecForAmountString,
|
||||||
|
codecForTimestamp,
|
||||||
|
codecOptional,
|
||||||
|
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";
|
||||||
|
|
||||||
|
const logger = new Logger("operations/peer-to-peer.ts");
|
||||||
|
|
||||||
|
interface SelectedPeerCoin {
|
||||||
|
coinPub: string;
|
||||||
|
coinPriv: string;
|
||||||
|
contribution: AmountString;
|
||||||
|
denomPubHash: string;
|
||||||
|
denomSig: UnblindedSignature;
|
||||||
|
ageCommitmentProof: AgeCommitmentProof | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PeerCoinSelectionDetails {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Info of Coins that were selected.
|
||||||
|
*/
|
||||||
|
coins: SelectedPeerCoin[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How much of the deposit fees is the customer paying?
|
||||||
|
*/
|
||||||
|
depositFees: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a selected coin for peer to peer payments.
|
||||||
|
*/
|
||||||
|
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 type SelectPeerCoinsResult =
|
||||||
|
| { type: "success"; result: PeerCoinSelectionDetails }
|
||||||
|
| {
|
||||||
|
type: "failure";
|
||||||
|
insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 interface PeerCoinSelectionRequest {
|
||||||
|
instructedAmount: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instruct the coin selection to repair this coin
|
||||||
|
* selection instead of selecting completely new coins.
|
||||||
|
*/
|
||||||
|
repair?: {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
coinPubs: CoinPublicKeyString[];
|
||||||
|
contribs: AmountJson[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function selectPeerCoins(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: PeerCoinSelectionRequest,
|
||||||
|
): Promise<SelectPeerCoinsResult> {
|
||||||
|
const instructedAmount = req.instructedAmount;
|
||||||
|
if (Amounts.isZero(instructedAmount)) {
|
||||||
|
// Other parts of the code assume that we have at least
|
||||||
|
// one coin to spend.
|
||||||
|
throw new Error("amount of zero not allowed");
|
||||||
|
}
|
||||||
|
return await ws.db
|
||||||
|
.mktx((x) => [
|
||||||
|
x.exchanges,
|
||||||
|
x.contractTerms,
|
||||||
|
x.coins,
|
||||||
|
x.coinAvailability,
|
||||||
|
x.denominations,
|
||||||
|
x.refreshGroups,
|
||||||
|
x.peerPushPaymentInitiations,
|
||||||
|
])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const exchanges = await tx.exchanges.iter().toArray();
|
||||||
|
const exchangeFeeGap: { [url: string]: AmountJson } = {};
|
||||||
|
const currency = Amounts.currencyOf(instructedAmount);
|
||||||
|
for (const exch of exchanges) {
|
||||||
|
if (exch.detailsPointer?.currency !== currency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// FIXME: Can't we do this faster by using coinAvailability?
|
||||||
|
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: Amounts.parseOrThrow(denom.feeDeposit),
|
||||||
|
value: Amounts.parseOrThrow(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.zeroOfCurrency(currency);
|
||||||
|
let depositFeesAcc = Amounts.zeroOfCurrency(currency);
|
||||||
|
const resCoins: {
|
||||||
|
coinPub: string;
|
||||||
|
coinPriv: string;
|
||||||
|
contribution: AmountString;
|
||||||
|
denomPubHash: string;
|
||||||
|
denomSig: UnblindedSignature;
|
||||||
|
ageCommitmentProof: AgeCommitmentProof | undefined;
|
||||||
|
}[] = [];
|
||||||
|
let lastDepositFee = Amounts.zeroOfCurrency(currency);
|
||||||
|
|
||||||
|
if (req.repair) {
|
||||||
|
for (let i = 0; i < req.repair.coinPubs.length; i++) {
|
||||||
|
const contrib = req.repair.contribs[i];
|
||||||
|
const coin = await tx.coins.get(req.repair.coinPubs[i]);
|
||||||
|
if (!coin) {
|
||||||
|
throw Error("repair not possible, coin not found");
|
||||||
|
}
|
||||||
|
const denom = await ws.getDenomInfo(
|
||||||
|
ws,
|
||||||
|
tx,
|
||||||
|
coin.exchangeBaseUrl,
|
||||||
|
coin.denomPubHash,
|
||||||
|
);
|
||||||
|
checkDbInvariant(!!denom);
|
||||||
|
resCoins.push({
|
||||||
|
coinPriv: coin.coinPriv,
|
||||||
|
coinPub: coin.coinPub,
|
||||||
|
contribution: Amounts.stringify(contrib),
|
||||||
|
denomPubHash: coin.denomPubHash,
|
||||||
|
denomSig: coin.denomSig,
|
||||||
|
ageCommitmentProof: coin.ageCommitmentProof,
|
||||||
|
});
|
||||||
|
const depositFee = Amounts.parseOrThrow(denom.feeDeposit);
|
||||||
|
lastDepositFee = depositFee;
|
||||||
|
amountAcc = Amounts.add(
|
||||||
|
amountAcc,
|
||||||
|
Amounts.sub(contrib, depositFee).amount,
|
||||||
|
).amount;
|
||||||
|
depositFeesAcc = Amounts.add(depositFeesAcc, depositFee).amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const coin of coinInfos) {
|
||||||
|
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
lastDepositFee = coin.feeDeposit;
|
||||||
|
}
|
||||||
|
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
|
||||||
|
const res: PeerCoinSelectionDetails = {
|
||||||
|
exchangeBaseUrl: exch.baseUrl,
|
||||||
|
coins: resCoins,
|
||||||
|
depositFees: depositFeesAcc,
|
||||||
|
};
|
||||||
|
return { type: "success", result: res };
|
||||||
|
}
|
||||||
|
const diff = Amounts.sub(instructedAmount, amountAcc).amount;
|
||||||
|
exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We were unable to select coins.
|
||||||
|
// Now we need to produce error details.
|
||||||
|
|
||||||
|
const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
|
||||||
|
currency,
|
||||||
|
});
|
||||||
|
|
||||||
|
const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
|
||||||
|
|
||||||
|
let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency);
|
||||||
|
|
||||||
|
for (const exch of exchanges) {
|
||||||
|
if (exch.detailsPointer?.currency !== currency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
|
||||||
|
currency,
|
||||||
|
restrictExchangeTo: exch.baseUrl,
|
||||||
|
});
|
||||||
|
let gap =
|
||||||
|
exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency);
|
||||||
|
if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) {
|
||||||
|
// Show fee gap only if we should've been able to pay with the material amount
|
||||||
|
gap = Amounts.zeroOfCurrency(currency);
|
||||||
|
}
|
||||||
|
perExchange[exch.baseUrl] = {
|
||||||
|
balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
|
||||||
|
balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
|
||||||
|
feeGapEstimate: Amounts.stringify(gap),
|
||||||
|
};
|
||||||
|
|
||||||
|
maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errDetails: PayPeerInsufficientBalanceDetails = {
|
||||||
|
amountRequested: Amounts.stringify(instructedAmount),
|
||||||
|
balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
|
||||||
|
balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
|
||||||
|
feeGapEstimate: Amounts.stringify(maxFeeGapEstimate),
|
||||||
|
perExchange,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { type: "failure", insufficientBalanceDetails: errDetails };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -0,0 +1,910 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022-2023 Taler Systems S.A.
|
||||||
|
|
||||||
|
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/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
|
Amounts,
|
||||||
|
CancellationToken,
|
||||||
|
CheckPeerPullCreditRequest,
|
||||||
|
CheckPeerPullCreditResponse,
|
||||||
|
ContractTermsUtil,
|
||||||
|
ExchangeReservePurseRequest,
|
||||||
|
HttpStatusCode,
|
||||||
|
InitiatePeerPullCreditRequest,
|
||||||
|
InitiatePeerPullCreditResponse,
|
||||||
|
Logger,
|
||||||
|
TalerPreciseTimestamp,
|
||||||
|
TransactionAction,
|
||||||
|
TransactionMajorState,
|
||||||
|
TransactionMinorState,
|
||||||
|
TransactionState,
|
||||||
|
TransactionType,
|
||||||
|
WalletAccountMergeFlags,
|
||||||
|
codecForAny,
|
||||||
|
codecForWalletKycUuid,
|
||||||
|
constructPayPullUri,
|
||||||
|
encodeCrock,
|
||||||
|
getRandomBytes,
|
||||||
|
j2s,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import {
|
||||||
|
readSuccessResponseJsonOrErrorCode,
|
||||||
|
readSuccessResponseJsonOrThrow,
|
||||||
|
throwUnexpectedRequestError,
|
||||||
|
} from "@gnu-taler/taler-util/http";
|
||||||
|
import {
|
||||||
|
PeerPullPaymentInitiationRecord,
|
||||||
|
PeerPullPaymentInitiationStatus,
|
||||||
|
WithdrawalGroupStatus,
|
||||||
|
WithdrawalRecordType,
|
||||||
|
updateExchangeFromUrl,
|
||||||
|
} from "../index.js";
|
||||||
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
|
import { PendingTaskType } from "../pending-types.js";
|
||||||
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
|
import {
|
||||||
|
OperationAttemptResult,
|
||||||
|
OperationAttemptResultType,
|
||||||
|
constructTaskIdentifier,
|
||||||
|
} from "../util/retries.js";
|
||||||
|
import {
|
||||||
|
LongpollResult,
|
||||||
|
resetOperationTimeout,
|
||||||
|
runLongpollAsync,
|
||||||
|
runOperationWithErrorReporting,
|
||||||
|
} from "./common.js";
|
||||||
|
import {
|
||||||
|
codecForExchangePurseStatus,
|
||||||
|
getMergeReserveInfo,
|
||||||
|
talerPaytoFromExchangeReserve,
|
||||||
|
} from "./pay-peer-common.js";
|
||||||
|
import {
|
||||||
|
constructTransactionIdentifier,
|
||||||
|
notifyTransition,
|
||||||
|
stopLongpolling,
|
||||||
|
} from "./transactions.js";
|
||||||
|
import {
|
||||||
|
checkWithdrawalKycStatus,
|
||||||
|
getExchangeWithdrawalInfo,
|
||||||
|
internalCreateWithdrawalGroup,
|
||||||
|
processWithdrawalGroup,
|
||||||
|
} from "./withdraw.js";
|
||||||
|
|
||||||
|
const logger = new Logger("pay-peer-pull-credit.ts");
|
||||||
|
|
||||||
|
export async function queryPurseForPeerPullCredit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pullIni: PeerPullPaymentInitiationRecord,
|
||||||
|
cancellationToken: CancellationToken,
|
||||||
|
): Promise<LongpollResult> {
|
||||||
|
const purseDepositUrl = new URL(
|
||||||
|
`purses/${pullIni.pursePub}/deposit`,
|
||||||
|
pullIni.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
purseDepositUrl.searchParams.set("timeout_ms", "30000");
|
||||||
|
logger.info(`querying purse status via ${purseDepositUrl.href}`);
|
||||||
|
const resp = await ws.http.get(purseDepositUrl.href, {
|
||||||
|
timeout: { d_ms: 60000 },
|
||||||
|
cancellationToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`purse status code: HTTP ${resp.status}`);
|
||||||
|
|
||||||
|
const result = await readSuccessResponseJsonOrErrorCode(
|
||||||
|
resp,
|
||||||
|
codecForExchangePurseStatus(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isError) {
|
||||||
|
logger.info(`got purse status error, EC=${result.talerErrorResponse.code}`);
|
||||||
|
if (resp.status === 404) {
|
||||||
|
return { ready: false };
|
||||||
|
} else {
|
||||||
|
throwUnexpectedRequestError(resp, result.talerErrorResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.response.deposit_timestamp) {
|
||||||
|
logger.info("purse not ready yet (no deposit)");
|
||||||
|
return { ready: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const reserve = await ws.db
|
||||||
|
.mktx((x) => [x.reserves])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return await tx.reserves.get(pullIni.mergeReserveRowId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!reserve) {
|
||||||
|
throw Error("reserve for peer pull credit not found in wallet DB");
|
||||||
|
}
|
||||||
|
|
||||||
|
await internalCreateWithdrawalGroup(ws, {
|
||||||
|
amount: Amounts.parseOrThrow(pullIni.amount),
|
||||||
|
wgInfo: {
|
||||||
|
withdrawalType: WithdrawalRecordType.PeerPullCredit,
|
||||||
|
contractTerms: pullIni.contractTerms,
|
||||||
|
contractPriv: pullIni.contractPriv,
|
||||||
|
},
|
||||||
|
forcedWithdrawalGroupId: pullIni.withdrawalGroupId,
|
||||||
|
exchangeBaseUrl: pullIni.exchangeBaseUrl,
|
||||||
|
reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
|
||||||
|
reserveKeyPair: {
|
||||||
|
priv: reserve.reservePriv,
|
||||||
|
pub: reserve.reservePub,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const finPi = await tx.peerPullPaymentInitiations.get(pullIni.pursePub);
|
||||||
|
if (!finPi) {
|
||||||
|
logger.warn("peerPullPaymentInitiation not found anymore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (finPi.status === PeerPullPaymentInitiationStatus.PendingReady) {
|
||||||
|
finPi.status = PeerPullPaymentInitiationStatus.DonePurseDeposited;
|
||||||
|
}
|
||||||
|
await tx.peerPullPaymentInitiations.put(finPi);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processPeerPullCredit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
): Promise<OperationAttemptResult> {
|
||||||
|
const pullIni = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.peerPullPaymentInitiations.get(pursePub);
|
||||||
|
});
|
||||||
|
if (!pullIni) {
|
||||||
|
throw Error("peer pull payment initiation not found in database");
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryTag = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullCredit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
|
||||||
|
// We're already running!
|
||||||
|
if (ws.activeLongpoll[retryTag]) {
|
||||||
|
logger.info("peer-pull-credit already in long-polling, returning!");
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Longpoll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace(`processing ${retryTag}, status=${pullIni.status}`);
|
||||||
|
|
||||||
|
switch (pullIni.status) {
|
||||||
|
case PeerPullPaymentInitiationStatus.DonePurseDeposited: {
|
||||||
|
// We implement this case so that the "retry" action on a peer-pull-credit transaction
|
||||||
|
// also retries the withdrawal task.
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
"peer pull payment initiation is already finished, retrying withdrawal",
|
||||||
|
);
|
||||||
|
|
||||||
|
const withdrawalGroupId = pullIni.withdrawalGroupId;
|
||||||
|
|
||||||
|
if (withdrawalGroupId) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.Withdraw,
|
||||||
|
withdrawalGroupId,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
await resetOperationTimeout(ws, taskId);
|
||||||
|
await runOperationWithErrorReporting(ws, taskId, () =>
|
||||||
|
processWithdrawalGroup(ws, withdrawalGroupId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Finished,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
||||||
|
runLongpollAsync(ws, retryTag, async (cancellationToken) =>
|
||||||
|
queryPurseForPeerPullCredit(ws, pullIni, cancellationToken),
|
||||||
|
);
|
||||||
|
logger.trace(
|
||||||
|
"returning early from processPeerPullCredit for long-polling in background",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Longpoll,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: {
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullCredit,
|
||||||
|
pursePub: pullIni.pursePub,
|
||||||
|
});
|
||||||
|
if (pullIni.kycInfo) {
|
||||||
|
await checkWithdrawalKycStatus(
|
||||||
|
ws,
|
||||||
|
pullIni.exchangeBaseUrl,
|
||||||
|
transactionId,
|
||||||
|
pullIni.kycInfo,
|
||||||
|
"individual",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Error(`unknown PeerPullPaymentInitiationStatus ${pullIni.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeReserve = await ws.db
|
||||||
|
.mktx((x) => [x.reserves])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.reserves.get(pullIni.mergeReserveRowId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mergeReserve) {
|
||||||
|
throw Error("merge reserve for peer pull payment not found in database");
|
||||||
|
}
|
||||||
|
|
||||||
|
const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount));
|
||||||
|
|
||||||
|
const reservePayto = talerPaytoFromExchangeReserve(
|
||||||
|
pullIni.exchangeBaseUrl,
|
||||||
|
mergeReserve.reservePub,
|
||||||
|
);
|
||||||
|
|
||||||
|
const econtractResp = await ws.cryptoApi.encryptContractForDeposit({
|
||||||
|
contractPriv: pullIni.contractPriv,
|
||||||
|
contractPub: pullIni.contractPub,
|
||||||
|
contractTerms: pullIni.contractTerms,
|
||||||
|
pursePriv: pullIni.pursePriv,
|
||||||
|
pursePub: pullIni.pursePub,
|
||||||
|
});
|
||||||
|
|
||||||
|
const purseExpiration = pullIni.contractTerms.purse_expiration;
|
||||||
|
const sigRes = await ws.cryptoApi.signReservePurseCreate({
|
||||||
|
contractTermsHash: pullIni.contractTermsHash,
|
||||||
|
flags: WalletAccountMergeFlags.CreateWithPurseFee,
|
||||||
|
mergePriv: pullIni.mergePriv,
|
||||||
|
mergeTimestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp),
|
||||||
|
purseAmount: pullIni.contractTerms.amount,
|
||||||
|
purseExpiration: purseExpiration,
|
||||||
|
purseFee: purseFee,
|
||||||
|
pursePriv: pullIni.pursePriv,
|
||||||
|
pursePub: pullIni.pursePub,
|
||||||
|
reservePayto,
|
||||||
|
reservePriv: mergeReserve.reservePriv,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reservePurseReqBody: ExchangeReservePurseRequest = {
|
||||||
|
merge_sig: sigRes.mergeSig,
|
||||||
|
merge_timestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp),
|
||||||
|
h_contract_terms: pullIni.contractTermsHash,
|
||||||
|
merge_pub: pullIni.mergePub,
|
||||||
|
min_age: 0,
|
||||||
|
purse_expiration: purseExpiration,
|
||||||
|
purse_fee: purseFee,
|
||||||
|
purse_pub: pullIni.pursePub,
|
||||||
|
purse_sig: sigRes.purseSig,
|
||||||
|
purse_value: pullIni.contractTerms.amount,
|
||||||
|
reserve_sig: sigRes.accountSig,
|
||||||
|
econtract: econtractResp.econtract,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
|
||||||
|
|
||||||
|
const reservePurseMergeUrl = new URL(
|
||||||
|
`reserves/${mergeReserve.reservePub}/purse`,
|
||||||
|
pullIni.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const httpResp = await ws.http.postJson(
|
||||||
|
reservePurseMergeUrl.href,
|
||||||
|
reservePurseReqBody,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
|
||||||
|
const respJson = await httpResp.json();
|
||||||
|
const kycPending = codecForWalletKycUuid().decode(respJson);
|
||||||
|
logger.info(`kyc uuid response: ${j2s(kycPending)}`);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const peerIni = await tx.peerPullPaymentInitiations.get(pursePub);
|
||||||
|
if (!peerIni) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
peerIni.kycInfo = {
|
||||||
|
paytoHash: kycPending.h_payto,
|
||||||
|
requirementRow: kycPending.requirement_row,
|
||||||
|
};
|
||||||
|
peerIni.status =
|
||||||
|
PeerPullPaymentInitiationStatus.PendingMergeKycRequired;
|
||||||
|
await tx.peerPullPaymentInitiations.put(peerIni);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Pending,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
||||||
|
|
||||||
|
logger.info(`reserve merge response: ${j2s(resp)}`);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pi2 = await tx.peerPullPaymentInitiations.get(pursePub);
|
||||||
|
if (!pi2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pi2.status = PeerPullPaymentInitiationStatus.PendingReady;
|
||||||
|
await tx.peerPullPaymentInitiations.put(pi2);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Finished,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check fees and available exchanges for a peer push payment initiation.
|
||||||
|
*/
|
||||||
|
export async function checkPeerPullPaymentInitiation(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: CheckPeerPullCreditRequest,
|
||||||
|
): Promise<CheckPeerPullCreditResponse> {
|
||||||
|
// FIXME: We don't support exchanges with purse fees yet.
|
||||||
|
// Select an exchange where we have money in the specified currency
|
||||||
|
// FIXME: How do we handle regional currency scopes here? Is it an additional input?
|
||||||
|
|
||||||
|
logger.trace("checking peer-pull-credit fees");
|
||||||
|
|
||||||
|
const currency = Amounts.currencyOf(req.amount);
|
||||||
|
let exchangeUrl;
|
||||||
|
if (req.exchangeBaseUrl) {
|
||||||
|
exchangeUrl = req.exchangeBaseUrl;
|
||||||
|
} else {
|
||||||
|
exchangeUrl = await getPreferredExchangeForCurrency(ws, currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exchangeUrl) {
|
||||||
|
throw Error("no exchange found for initiating a peer pull payment");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace(`found ${exchangeUrl} as preferred exchange`);
|
||||||
|
|
||||||
|
const wi = await getExchangeWithdrawalInfo(
|
||||||
|
ws,
|
||||||
|
exchangeUrl,
|
||||||
|
Amounts.parseOrThrow(req.amount),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.trace(`got withdrawal info`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exchangeBaseUrl: exchangeUrl,
|
||||||
|
amountEffective: wi.withdrawalAmountEffective,
|
||||||
|
amountRaw: req.amount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a preferred exchange based on when we withdrew last from this exchange.
|
||||||
|
*/
|
||||||
|
async function getPreferredExchangeForCurrency(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
currency: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
// Find an exchange with the matching currency.
|
||||||
|
// Prefer exchanges with the most recent withdrawal.
|
||||||
|
const url = await ws.db
|
||||||
|
.mktx((x) => [x.exchanges])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
const exchanges = await tx.exchanges.iter().toArray();
|
||||||
|
let candidate = undefined;
|
||||||
|
for (const e of exchanges) {
|
||||||
|
if (e.detailsPointer?.currency !== currency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!candidate) {
|
||||||
|
candidate = e;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (candidate.lastWithdrawal && !e.lastWithdrawal) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (candidate.lastWithdrawal && e.lastWithdrawal) {
|
||||||
|
if (
|
||||||
|
AbsoluteTime.cmp(
|
||||||
|
AbsoluteTime.fromPreciseTimestamp(e.lastWithdrawal),
|
||||||
|
AbsoluteTime.fromPreciseTimestamp(candidate.lastWithdrawal),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
candidate = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (candidate) {
|
||||||
|
return candidate.baseUrl;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate a peer pull payment.
|
||||||
|
*/
|
||||||
|
export async function initiatePeerPullPayment(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: InitiatePeerPullCreditRequest,
|
||||||
|
): Promise<InitiatePeerPullCreditResponse> {
|
||||||
|
const currency = Amounts.currencyOf(req.partialContractTerms.amount);
|
||||||
|
let maybeExchangeBaseUrl: string | undefined;
|
||||||
|
if (req.exchangeBaseUrl) {
|
||||||
|
maybeExchangeBaseUrl = req.exchangeBaseUrl;
|
||||||
|
} else {
|
||||||
|
maybeExchangeBaseUrl = await getPreferredExchangeForCurrency(ws, currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!maybeExchangeBaseUrl) {
|
||||||
|
throw Error("no exchange found for initiating a peer pull payment");
|
||||||
|
}
|
||||||
|
|
||||||
|
const exchangeBaseUrl = maybeExchangeBaseUrl;
|
||||||
|
|
||||||
|
await updateExchangeFromUrl(ws, exchangeBaseUrl);
|
||||||
|
|
||||||
|
const mergeReserveInfo = await getMergeReserveInfo(ws, {
|
||||||
|
exchangeBaseUrl: exchangeBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergeTimestamp = TalerPreciseTimestamp.now();
|
||||||
|
|
||||||
|
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
|
||||||
|
const contractTerms = req.partialContractTerms;
|
||||||
|
|
||||||
|
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
|
||||||
|
|
||||||
|
const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
|
||||||
|
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
|
const mergeReserveRowId = mergeReserveInfo.rowId;
|
||||||
|
checkDbInvariant(!!mergeReserveRowId);
|
||||||
|
|
||||||
|
const wi = await getExchangeWithdrawalInfo(
|
||||||
|
ws,
|
||||||
|
exchangeBaseUrl,
|
||||||
|
Amounts.parseOrThrow(req.partialContractTerms.amount),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await tx.peerPullPaymentInitiations.put({
|
||||||
|
amount: req.partialContractTerms.amount,
|
||||||
|
contractTermsHash: hContractTerms,
|
||||||
|
exchangeBaseUrl: exchangeBaseUrl,
|
||||||
|
pursePriv: pursePair.priv,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
mergePriv: mergePair.priv,
|
||||||
|
mergePub: mergePair.pub,
|
||||||
|
status: PeerPullPaymentInitiationStatus.PendingCreatePurse,
|
||||||
|
contractTerms: contractTerms,
|
||||||
|
mergeTimestamp,
|
||||||
|
mergeReserveRowId: mergeReserveRowId,
|
||||||
|
contractPriv: contractKeyPair.priv,
|
||||||
|
contractPub: contractKeyPair.pub,
|
||||||
|
withdrawalGroupId,
|
||||||
|
estimatedAmountEffective: wi.withdrawalAmountEffective,
|
||||||
|
});
|
||||||
|
await tx.contractTerms.put({
|
||||||
|
contractTermsRaw: contractTerms,
|
||||||
|
h: hContractTerms,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: Should we somehow signal to the client
|
||||||
|
// whether purse creation has failed, or does the client/
|
||||||
|
// check this asynchronously from the transaction status?
|
||||||
|
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullCredit,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runOperationWithErrorReporting(ws, taskId, async () => {
|
||||||
|
return processPeerPullCredit(ws, pursePair.pub);
|
||||||
|
});
|
||||||
|
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullCredit,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
talerUri: constructPayPullUri({
|
||||||
|
exchangeBaseUrl: exchangeBaseUrl,
|
||||||
|
contractPriv: contractKeyPair.priv,
|
||||||
|
}),
|
||||||
|
transactionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function suspendPeerPullCreditTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullCredit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullCredit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub);
|
||||||
|
if (!pullCreditRec) {
|
||||||
|
logger.warn(`peer pull credit ${pursePub} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined;
|
||||||
|
switch (pullCreditRec.status) {
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.SuspendedCreatePurse;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.SuspendedWithdrawing;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.SuspendedReady;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
newStatus =
|
||||||
|
PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.DonePurseDeposited:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
||||||
|
case PeerPullPaymentInitiationStatus.Aborted:
|
||||||
|
case PeerPullPaymentInitiationStatus.Failed:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pullCreditRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
||||||
|
pullCreditRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
||||||
|
await tx.peerPullPaymentInitiations.put(pullCreditRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function abortPeerPullCreditTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullCredit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullCredit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub);
|
||||||
|
if (!pullCreditRec) {
|
||||||
|
logger.warn(`peer pull credit ${pursePub} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined;
|
||||||
|
switch (pullCreditRec.status) {
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
||||||
|
throw Error("can't abort anymore");
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.DonePurseDeposited:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
||||||
|
case PeerPullPaymentInitiationStatus.Aborted:
|
||||||
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
case PeerPullPaymentInitiationStatus.Failed:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pullCreditRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
||||||
|
pullCreditRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
||||||
|
await tx.peerPullPaymentInitiations.put(pullCreditRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function failPeerPullCreditTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullCredit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullCredit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub);
|
||||||
|
if (!pullCreditRec) {
|
||||||
|
logger.warn(`peer pull credit ${pursePub} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined;
|
||||||
|
switch (pullCreditRec.status) {
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
||||||
|
case PeerPullPaymentInitiationStatus.DonePurseDeposited:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
||||||
|
case PeerPullPaymentInitiationStatus.Aborted:
|
||||||
|
case PeerPullPaymentInitiationStatus.Failed:
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.Failed;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pullCreditRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
||||||
|
pullCreditRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
||||||
|
await tx.peerPullPaymentInitiations.put(pullCreditRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resumePeerPullCreditTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullCredit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullCredit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub);
|
||||||
|
if (!pullCreditRec) {
|
||||||
|
logger.warn(`peer pull credit ${pursePub} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined;
|
||||||
|
switch (pullCreditRec.status) {
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
||||||
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
case PeerPullPaymentInitiationStatus.DonePurseDeposited:
|
||||||
|
case PeerPullPaymentInitiationStatus.Failed:
|
||||||
|
case PeerPullPaymentInitiationStatus.Aborted:
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.PendingCreatePurse;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.PendingMergeKycRequired;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.PendingReady;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.PendingWithdrawing;
|
||||||
|
break;
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pullCreditRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
||||||
|
pullCreditRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
||||||
|
await tx.peerPullPaymentInitiations.put(pullCreditRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
ws.workAvailable.trigger();
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computePeerPullCreditTransactionState(
|
||||||
|
pullCreditRecord: PeerPullPaymentInitiationRecord,
|
||||||
|
): TransactionState {
|
||||||
|
switch (pullCreditRecord.status) {
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.CreatePurse,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.MergeKycRequired,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.Ready,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.DonePurseDeposited:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Done,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.Withdraw,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
minor: TransactionMinorState.CreatePurse,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
minor: TransactionMinorState.Ready,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.Withdraw,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
minor: TransactionMinorState.MergeKycRequired,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.Aborted:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Aborted,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Aborting,
|
||||||
|
minor: TransactionMinorState.DeletePurse,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.Failed:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Failed,
|
||||||
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Aborting,
|
||||||
|
minor: TransactionMinorState.DeletePurse,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computePeerPullCreditTransactionActions(
|
||||||
|
pullCreditRecord: PeerPullPaymentInitiationRecord,
|
||||||
|
): TransactionAction[] {
|
||||||
|
switch (pullCreditRecord.status) {
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||||
|
case PeerPullPaymentInitiationStatus.DonePurseDeposited:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Resume];
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||||
|
case PeerPullPaymentInitiationStatus.Aborted:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||||
|
case PeerPullPaymentInitiationStatus.Failed:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||||
|
}
|
||||||
|
}
|
604
packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
Normal file
604
packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
Normal file
@ -0,0 +1,604 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022-2023 Taler Systems S.A.
|
||||||
|
|
||||||
|
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/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConfirmPeerPullDebitRequest,
|
||||||
|
AcceptPeerPullPaymentResponse,
|
||||||
|
Amounts,
|
||||||
|
j2s,
|
||||||
|
TalerError,
|
||||||
|
TalerErrorCode,
|
||||||
|
TransactionType,
|
||||||
|
RefreshReason,
|
||||||
|
Logger,
|
||||||
|
PeerContractTerms,
|
||||||
|
PreparePeerPullDebitRequest,
|
||||||
|
PreparePeerPullDebitResponse,
|
||||||
|
TalerPreciseTimestamp,
|
||||||
|
codecForExchangeGetContractResponse,
|
||||||
|
codecForPeerContractTerms,
|
||||||
|
decodeCrock,
|
||||||
|
eddsaGetPublic,
|
||||||
|
encodeCrock,
|
||||||
|
getRandomBytes,
|
||||||
|
parsePayPullUri,
|
||||||
|
TransactionAction,
|
||||||
|
TransactionMajorState,
|
||||||
|
TransactionMinorState,
|
||||||
|
TransactionState,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import {
|
||||||
|
InternalWalletState,
|
||||||
|
PeerPullDebitRecordStatus,
|
||||||
|
PeerPullPaymentIncomingRecord,
|
||||||
|
PendingTaskType,
|
||||||
|
} from "../index.js";
|
||||||
|
import { TaskIdentifiers, constructTaskIdentifier } from "../util/retries.js";
|
||||||
|
import { spendCoins, runOperationWithErrorReporting } from "./common.js";
|
||||||
|
import {
|
||||||
|
codecForExchangePurseStatus,
|
||||||
|
getTotalPeerPaymentCost,
|
||||||
|
selectPeerCoins,
|
||||||
|
} from "./pay-peer-common.js";
|
||||||
|
import { processPeerPullDebit } from "./pay-peer-push-credit.js";
|
||||||
|
import {
|
||||||
|
constructTransactionIdentifier,
|
||||||
|
notifyTransition,
|
||||||
|
stopLongpolling,
|
||||||
|
} from "./transactions.js";
|
||||||
|
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
|
||||||
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
|
|
||||||
|
const logger = new Logger("pay-peer-pull-debit.ts");
|
||||||
|
|
||||||
|
export async function confirmPeerPullDebit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: ConfirmPeerPullDebitRequest,
|
||||||
|
): 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 = await selectPeerCoins(ws, { instructedAmount });
|
||||||
|
logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
|
||||||
|
|
||||||
|
if (coinSelRes.type !== "success") {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
|
||||||
|
{
|
||||||
|
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sel = coinSelRes.result;
|
||||||
|
|
||||||
|
const totalAmount = await getTotalPeerPaymentCost(
|
||||||
|
ws,
|
||||||
|
coinSelRes.result.coins,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ppi = await ws.db
|
||||||
|
.mktx((x) => [
|
||||||
|
x.exchanges,
|
||||||
|
x.coins,
|
||||||
|
x.denominations,
|
||||||
|
x.refreshGroups,
|
||||||
|
x.peerPullPaymentIncoming,
|
||||||
|
x.coinAvailability,
|
||||||
|
])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await spendCoins(ws, tx, {
|
||||||
|
// allocationId: `txn:peer-pull-debit:${req.peerPullPaymentIncomingId}`,
|
||||||
|
allocationId: constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId: 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();
|
||||||
|
}
|
||||||
|
if (pi.status === PeerPullDebitRecordStatus.DialogProposed) {
|
||||||
|
pi.status = PeerPullDebitRecordStatus.PendingDeposit;
|
||||||
|
pi.coinSel = {
|
||||||
|
coinPubs: sel.coins.map((x) => x.coinPub),
|
||||||
|
contributions: sel.coins.map((x) => x.contribution),
|
||||||
|
totalCost: Amounts.stringify(totalAmount),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await tx.peerPullPaymentIncoming.put(pi);
|
||||||
|
return pi;
|
||||||
|
});
|
||||||
|
|
||||||
|
await runOperationWithErrorReporting(
|
||||||
|
ws,
|
||||||
|
TaskIdentifiers.forPeerPullPaymentDebit(ppi),
|
||||||
|
async () => {
|
||||||
|
return processPeerPullDebit(ws, ppi.peerPullPaymentIncomingId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId: req.peerPullPaymentIncomingId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up information about an incoming peer pull payment.
|
||||||
|
* Store the results in the wallet DB.
|
||||||
|
*/
|
||||||
|
export async function preparePeerPullDebit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: PreparePeerPullDebitRequest,
|
||||||
|
): Promise<PreparePeerPullDebitResponse> {
|
||||||
|
const uri = parsePayPullUri(req.talerUri);
|
||||||
|
|
||||||
|
if (!uri) {
|
||||||
|
throw Error("got invalid taler://pay-pull URI");
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPullIncomingRecord = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.peerPullPaymentIncoming.indexes.byExchangeAndContractPriv.get([
|
||||||
|
uri.exchangeBaseUrl,
|
||||||
|
uri.contractPriv,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingPullIncomingRecord) {
|
||||||
|
return {
|
||||||
|
amount: existingPullIncomingRecord.contractTerms.amount,
|
||||||
|
amountRaw: existingPullIncomingRecord.contractTerms.amount,
|
||||||
|
amountEffective: existingPullIncomingRecord.totalCostEstimated,
|
||||||
|
contractTerms: existingPullIncomingRecord.contractTerms,
|
||||||
|
peerPullPaymentIncomingId:
|
||||||
|
existingPullIncomingRecord.peerPullPaymentIncomingId,
|
||||||
|
transactionId: constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId:
|
||||||
|
existingPullIncomingRecord.peerPullPaymentIncomingId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
let contractTerms: PeerContractTerms;
|
||||||
|
|
||||||
|
if (dec.contractTerms) {
|
||||||
|
contractTerms = codecForPeerContractTerms().decode(dec.contractTerms);
|
||||||
|
// FIXME: Check that the purseStatus balance matches contract terms amount
|
||||||
|
} else {
|
||||||
|
// FIXME: In this case, where do we get the purse expiration from?!
|
||||||
|
// https://bugs.gnunet.org/view.php?id=7706
|
||||||
|
throw Error("pull payments without contract terms not supported yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Why don't we compute the totalCost here?!
|
||||||
|
|
||||||
|
const instructedAmount = Amounts.parseOrThrow(contractTerms.amount);
|
||||||
|
|
||||||
|
const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
|
||||||
|
logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
|
||||||
|
|
||||||
|
if (coinSelRes.type !== "success") {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
|
||||||
|
{
|
||||||
|
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAmount = await getTotalPeerPaymentCost(
|
||||||
|
ws,
|
||||||
|
coinSelRes.result.coins,
|
||||||
|
);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await tx.peerPullPaymentIncoming.add({
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
contractPriv: contractPriv,
|
||||||
|
exchangeBaseUrl: exchangeBaseUrl,
|
||||||
|
pursePub: pursePub,
|
||||||
|
timestampCreated: TalerPreciseTimestamp.now(),
|
||||||
|
contractTerms,
|
||||||
|
status: PeerPullDebitRecordStatus.DialogProposed,
|
||||||
|
totalCostEstimated: Amounts.stringify(totalAmount),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: contractTerms.amount,
|
||||||
|
amountEffective: Amounts.stringify(totalAmount),
|
||||||
|
amountRaw: contractTerms.amount,
|
||||||
|
contractTerms: contractTerms,
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
transactionId: constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId: peerPullPaymentIncomingId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function suspendPeerPullDebitTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPullPaymentIncomingId: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pullDebitRec = await tx.peerPullPaymentIncoming.get(
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!pullDebitRec) {
|
||||||
|
logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
|
||||||
|
switch (pullDebitRec.status) {
|
||||||
|
case PeerPullDebitRecordStatus.DialogProposed:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.DonePaid:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.PendingDeposit:
|
||||||
|
newStatus = PeerPullDebitRecordStatus.SuspendedDeposit;
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedDeposit:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.Aborted:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.AbortingRefresh:
|
||||||
|
newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh;
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.Failed:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pullDebitRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
|
||||||
|
pullDebitRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
|
||||||
|
await tx.peerPullPaymentIncoming.put(pullDebitRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function abortPeerPullDebitTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPullPaymentIncomingId: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pullDebitRec = await tx.peerPullPaymentIncoming.get(
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!pullDebitRec) {
|
||||||
|
logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
|
||||||
|
switch (pullDebitRec.status) {
|
||||||
|
case PeerPullDebitRecordStatus.DialogProposed:
|
||||||
|
newStatus = PeerPullDebitRecordStatus.Aborted;
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.DonePaid:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.PendingDeposit:
|
||||||
|
newStatus = PeerPullDebitRecordStatus.AbortingRefresh;
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedDeposit:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.Aborted:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.AbortingRefresh:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.Failed:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pullDebitRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
|
||||||
|
pullDebitRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
|
||||||
|
await tx.peerPullPaymentIncoming.put(pullDebitRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function failPeerPullDebitTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPullPaymentIncomingId: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pullDebitRec = await tx.peerPullPaymentIncoming.get(
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!pullDebitRec) {
|
||||||
|
logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
|
||||||
|
switch (pullDebitRec.status) {
|
||||||
|
case PeerPullDebitRecordStatus.DialogProposed:
|
||||||
|
newStatus = PeerPullDebitRecordStatus.Aborted;
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.DonePaid:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.PendingDeposit:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedDeposit:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.Aborted:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.Failed:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
|
||||||
|
case PeerPullDebitRecordStatus.AbortingRefresh:
|
||||||
|
// FIXME: abort underlying refresh!
|
||||||
|
newStatus = PeerPullDebitRecordStatus.Failed;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pullDebitRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
|
||||||
|
pullDebitRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
|
||||||
|
await tx.peerPullPaymentIncoming.put(pullDebitRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resumePeerPullDebitTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPullPaymentIncomingId: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPullDebit,
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pullDebitRec = await tx.peerPullPaymentIncoming.get(
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!pullDebitRec) {
|
||||||
|
logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
|
||||||
|
switch (pullDebitRec.status) {
|
||||||
|
case PeerPullDebitRecordStatus.DialogProposed:
|
||||||
|
case PeerPullDebitRecordStatus.DonePaid:
|
||||||
|
case PeerPullDebitRecordStatus.PendingDeposit:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedDeposit:
|
||||||
|
newStatus = PeerPullDebitRecordStatus.PendingDeposit;
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.Aborted:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.AbortingRefresh:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.Failed:
|
||||||
|
break;
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
|
||||||
|
newStatus = PeerPullDebitRecordStatus.AbortingRefresh;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pullDebitRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
|
||||||
|
pullDebitRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
|
||||||
|
await tx.peerPullPaymentIncoming.put(pullDebitRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
ws.workAvailable.trigger();
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computePeerPullDebitTransactionState(
|
||||||
|
pullDebitRecord: PeerPullPaymentIncomingRecord,
|
||||||
|
): TransactionState {
|
||||||
|
switch (pullDebitRecord.status) {
|
||||||
|
case PeerPullDebitRecordStatus.DialogProposed:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Dialog,
|
||||||
|
minor: TransactionMinorState.Proposed,
|
||||||
|
};
|
||||||
|
case PeerPullDebitRecordStatus.PendingDeposit:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.Deposit,
|
||||||
|
};
|
||||||
|
case PeerPullDebitRecordStatus.DonePaid:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Done,
|
||||||
|
};
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedDeposit:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
minor: TransactionMinorState.Deposit,
|
||||||
|
};
|
||||||
|
case PeerPullDebitRecordStatus.Aborted:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Aborted,
|
||||||
|
};
|
||||||
|
case PeerPullDebitRecordStatus.AbortingRefresh:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Aborting,
|
||||||
|
minor: TransactionMinorState.Refresh,
|
||||||
|
};
|
||||||
|
case PeerPullDebitRecordStatus.Failed:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Failed,
|
||||||
|
};
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.SuspendedAborting,
|
||||||
|
minor: TransactionMinorState.Refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computePeerPullDebitTransactionActions(
|
||||||
|
pullDebitRecord: PeerPullPaymentIncomingRecord,
|
||||||
|
): TransactionAction[] {
|
||||||
|
switch (pullDebitRecord.status) {
|
||||||
|
case PeerPullDebitRecordStatus.DialogProposed:
|
||||||
|
return [];
|
||||||
|
case PeerPullDebitRecordStatus.PendingDeposit:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||||
|
case PeerPullDebitRecordStatus.DonePaid:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedDeposit:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||||
|
case PeerPullDebitRecordStatus.Aborted:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPullDebitRecordStatus.AbortingRefresh:
|
||||||
|
return [TransactionAction.Fail, TransactionAction.Suspend];
|
||||||
|
case PeerPullDebitRecordStatus.Failed:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,770 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022-2023 Taler Systems S.A.
|
||||||
|
|
||||||
|
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/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
PreparePeerPushCredit,
|
||||||
|
PreparePeerPushCreditResponse,
|
||||||
|
parsePayPushUri,
|
||||||
|
codecForPeerContractTerms,
|
||||||
|
TransactionType,
|
||||||
|
encodeCrock,
|
||||||
|
eddsaGetPublic,
|
||||||
|
decodeCrock,
|
||||||
|
codecForExchangeGetContractResponse,
|
||||||
|
getRandomBytes,
|
||||||
|
ContractTermsUtil,
|
||||||
|
Amounts,
|
||||||
|
TalerPreciseTimestamp,
|
||||||
|
AcceptPeerPushPaymentResponse,
|
||||||
|
ConfirmPeerPushCreditRequest,
|
||||||
|
ExchangePurseMergeRequest,
|
||||||
|
HttpStatusCode,
|
||||||
|
PeerContractTerms,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
|
WalletAccountMergeFlags,
|
||||||
|
codecForAny,
|
||||||
|
codecForWalletKycUuid,
|
||||||
|
j2s,
|
||||||
|
Logger,
|
||||||
|
ExchangePurseDeposits,
|
||||||
|
TransactionAction,
|
||||||
|
TransactionMajorState,
|
||||||
|
TransactionMinorState,
|
||||||
|
TransactionState,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
|
||||||
|
import {
|
||||||
|
InternalWalletState,
|
||||||
|
PeerPullDebitRecordStatus,
|
||||||
|
PeerPushPaymentIncomingRecord,
|
||||||
|
PeerPushPaymentIncomingStatus,
|
||||||
|
PendingTaskType,
|
||||||
|
WithdrawalGroupStatus,
|
||||||
|
WithdrawalRecordType,
|
||||||
|
} from "../index.js";
|
||||||
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||||
|
import {
|
||||||
|
codecForExchangePurseStatus,
|
||||||
|
getMergeReserveInfo,
|
||||||
|
queryCoinInfosForSelection,
|
||||||
|
talerPaytoFromExchangeReserve,
|
||||||
|
} from "./pay-peer-common.js";
|
||||||
|
import { constructTransactionIdentifier, notifyTransition, stopLongpolling } from "./transactions.js";
|
||||||
|
import {
|
||||||
|
checkWithdrawalKycStatus,
|
||||||
|
getExchangeWithdrawalInfo,
|
||||||
|
internalCreateWithdrawalGroup,
|
||||||
|
} from "./withdraw.js";
|
||||||
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
|
import {
|
||||||
|
OperationAttemptResult,
|
||||||
|
OperationAttemptResultType,
|
||||||
|
constructTaskIdentifier,
|
||||||
|
} from "../util/retries.js";
|
||||||
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
|
|
||||||
|
const logger = new Logger("pay-peer-push-credit.ts");
|
||||||
|
|
||||||
|
export async function preparePeerPushCredit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: PreparePeerPushCredit,
|
||||||
|
): Promise<PreparePeerPushCreditResponse> {
|
||||||
|
const uri = parsePayPushUri(req.talerUri);
|
||||||
|
|
||||||
|
if (!uri) {
|
||||||
|
throw Error("got invalid taler://pay-push URI");
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await ws.db
|
||||||
|
.mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
const existingPushInc =
|
||||||
|
await tx.peerPushPaymentIncoming.indexes.byExchangeAndContractPriv.get([
|
||||||
|
uri.exchangeBaseUrl,
|
||||||
|
uri.contractPriv,
|
||||||
|
]);
|
||||||
|
if (!existingPushInc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existingContractTermsRec = await tx.contractTerms.get(
|
||||||
|
existingPushInc.contractTermsHash,
|
||||||
|
);
|
||||||
|
if (!existingContractTermsRec) {
|
||||||
|
throw Error(
|
||||||
|
"contract terms for peer push payment credit not found in database",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const existingContractTerms = codecForPeerContractTerms().decode(
|
||||||
|
existingContractTermsRec.contractTermsRaw,
|
||||||
|
);
|
||||||
|
return { existingPushInc, existingContractTerms };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return {
|
||||||
|
amount: existing.existingContractTerms.amount,
|
||||||
|
amountEffective: existing.existingPushInc.estimatedAmountEffective,
|
||||||
|
amountRaw: existing.existingContractTerms.amount,
|
||||||
|
contractTerms: existing.existingContractTerms,
|
||||||
|
peerPushPaymentIncomingId:
|
||||||
|
existing.existingPushInc.peerPushPaymentIncomingId,
|
||||||
|
transactionId: constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId:
|
||||||
|
existing.existingPushInc.peerPushPaymentIncomingId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
const contractTermsHash = ContractTermsUtil.hashContractTerms(
|
||||||
|
dec.contractTerms,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
|
const wi = await getExchangeWithdrawalInfo(
|
||||||
|
ws,
|
||||||
|
exchangeBaseUrl,
|
||||||
|
Amounts.parseOrThrow(purseStatus.balance),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await tx.peerPushPaymentIncoming.add({
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
contractPriv: contractPriv,
|
||||||
|
exchangeBaseUrl: exchangeBaseUrl,
|
||||||
|
mergePriv: dec.mergePriv,
|
||||||
|
pursePub: pursePub,
|
||||||
|
timestamp: TalerPreciseTimestamp.now(),
|
||||||
|
contractTermsHash,
|
||||||
|
status: PeerPushPaymentIncomingStatus.DialogProposed,
|
||||||
|
withdrawalGroupId,
|
||||||
|
currency: Amounts.currencyOf(purseStatus.balance),
|
||||||
|
estimatedAmountEffective: Amounts.stringify(
|
||||||
|
wi.withdrawalAmountEffective,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.contractTerms.put({
|
||||||
|
h: contractTermsHash,
|
||||||
|
contractTermsRaw: dec.contractTerms,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: purseStatus.balance,
|
||||||
|
amountEffective: wi.withdrawalAmountEffective,
|
||||||
|
amountRaw: purseStatus.balance,
|
||||||
|
contractTerms: dec.contractTerms,
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
transactionId: constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processPeerPushCredit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPushPaymentIncomingId: string,
|
||||||
|
): Promise<OperationAttemptResult> {
|
||||||
|
let peerInc: PeerPushPaymentIncomingRecord | undefined;
|
||||||
|
let contractTerms: PeerContractTerms | undefined;
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
peerInc = await tx.peerPushPaymentIncoming.get(peerPushPaymentIncomingId);
|
||||||
|
if (!peerInc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash);
|
||||||
|
if (ctRec) {
|
||||||
|
contractTerms = ctRec.contractTermsRaw;
|
||||||
|
}
|
||||||
|
await tx.peerPushPaymentIncoming.put(peerInc);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!peerInc) {
|
||||||
|
throw Error(
|
||||||
|
`can't accept unknown incoming p2p push payment (${peerPushPaymentIncomingId})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDbInvariant(!!contractTerms);
|
||||||
|
|
||||||
|
const amount = Amounts.parseOrThrow(contractTerms.amount);
|
||||||
|
|
||||||
|
if (
|
||||||
|
peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired &&
|
||||||
|
peerInc.kycInfo
|
||||||
|
) {
|
||||||
|
const txId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId,
|
||||||
|
});
|
||||||
|
await checkWithdrawalKycStatus(
|
||||||
|
ws,
|
||||||
|
peerInc.exchangeBaseUrl,
|
||||||
|
txId,
|
||||||
|
peerInc.kycInfo,
|
||||||
|
"individual",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(contractTerms),
|
||||||
|
flags: WalletAccountMergeFlags.MergeFullyPaidPurse,
|
||||||
|
mergePriv: peerInc.mergePriv,
|
||||||
|
mergeTimestamp: mergeTimestamp,
|
||||||
|
purseAmount: Amounts.stringify(amount),
|
||||||
|
purseExpiration: contractTerms.purse_expiration,
|
||||||
|
purseFee: Amounts.stringify(Amounts.zeroOfCurrency(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 mergeHttpResp = await ws.http.postJson(mergePurseUrl.href, mergeReq);
|
||||||
|
|
||||||
|
if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
|
||||||
|
const respJson = await mergeHttpResp.json();
|
||||||
|
const kycPending = codecForWalletKycUuid().decode(respJson);
|
||||||
|
logger.info(`kyc uuid response: ${j2s(kycPending)}`);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const peerInc = await tx.peerPushPaymentIncoming.get(
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!peerInc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
peerInc.kycInfo = {
|
||||||
|
paytoHash: kycPending.h_payto,
|
||||||
|
requirementRow: kycPending.requirement_row,
|
||||||
|
};
|
||||||
|
peerInc.status = PeerPushPaymentIncomingStatus.PendingMergeKycRequired;
|
||||||
|
await tx.peerPushPaymentIncoming.put(peerInc);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Pending,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace(`merge request: ${j2s(mergeReq)}`);
|
||||||
|
const res = await readSuccessResponseJsonOrThrow(
|
||||||
|
mergeHttpResp,
|
||||||
|
codecForAny(),
|
||||||
|
);
|
||||||
|
logger.trace(`merge response: ${j2s(res)}`);
|
||||||
|
|
||||||
|
await internalCreateWithdrawalGroup(ws, {
|
||||||
|
amount,
|
||||||
|
wgInfo: {
|
||||||
|
withdrawalType: WithdrawalRecordType.PeerPushCredit,
|
||||||
|
contractTerms,
|
||||||
|
},
|
||||||
|
forcedWithdrawalGroupId: peerInc.withdrawalGroupId,
|
||||||
|
exchangeBaseUrl: peerInc.exchangeBaseUrl,
|
||||||
|
reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
|
||||||
|
reserveKeyPair: {
|
||||||
|
priv: mergeReserveInfo.reservePriv,
|
||||||
|
pub: mergeReserveInfo.reservePub,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const peerInc = await tx.peerPushPaymentIncoming.get(
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!peerInc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
peerInc.status === PeerPushPaymentIncomingStatus.PendingMerge ||
|
||||||
|
peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired
|
||||||
|
) {
|
||||||
|
peerInc.status = PeerPushPaymentIncomingStatus.Done;
|
||||||
|
}
|
||||||
|
await tx.peerPushPaymentIncoming.put(peerInc);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Finished,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmPeerPushCredit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: ConfirmPeerPushCreditRequest,
|
||||||
|
): Promise<AcceptPeerPushPaymentResponse> {
|
||||||
|
let peerInc: PeerPushPaymentIncomingRecord | undefined;
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
peerInc = await tx.peerPushPaymentIncoming.get(
|
||||||
|
req.peerPushPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!peerInc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (peerInc.status === PeerPushPaymentIncomingStatus.DialogProposed) {
|
||||||
|
peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge;
|
||||||
|
}
|
||||||
|
await tx.peerPushPaymentIncoming.put(peerInc);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!peerInc) {
|
||||||
|
throw Error(
|
||||||
|
`can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.workAvailable.trigger();
|
||||||
|
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId: req.peerPushPaymentIncomingId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function processPeerPullDebit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPullPaymentIncomingId: string,
|
||||||
|
): Promise<OperationAttemptResult> {
|
||||||
|
const peerPullInc = await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId);
|
||||||
|
});
|
||||||
|
if (!peerPullInc) {
|
||||||
|
throw Error("peer pull debit not found");
|
||||||
|
}
|
||||||
|
if (peerPullInc.status === PeerPullDebitRecordStatus.PendingDeposit) {
|
||||||
|
const pursePub = peerPullInc.pursePub;
|
||||||
|
|
||||||
|
const coinSel = peerPullInc.coinSel;
|
||||||
|
if (!coinSel) {
|
||||||
|
throw Error("invalid state, no coins selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
const coins = await queryCoinInfosForSelection(ws, coinSel);
|
||||||
|
|
||||||
|
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
|
||||||
|
exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
|
||||||
|
pursePub: peerPullInc.pursePub,
|
||||||
|
coins,
|
||||||
|
});
|
||||||
|
|
||||||
|
const purseDepositUrl = new URL(
|
||||||
|
`purses/${pursePub}/deposit`,
|
||||||
|
peerPullInc.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const depositPayload: ExchangePurseDeposits = {
|
||||||
|
deposits: depositSigsResp.deposits,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (logger.shouldLogTrace()) {
|
||||||
|
logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpResp = await ws.http.postJson(
|
||||||
|
purseDepositUrl.href,
|
||||||
|
depositPayload,
|
||||||
|
);
|
||||||
|
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
||||||
|
logger.trace(`purse deposit response: ${j2s(resp)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pi = await tx.peerPullPaymentIncoming.get(
|
||||||
|
peerPullPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!pi) {
|
||||||
|
throw Error("peer pull payment not found anymore");
|
||||||
|
}
|
||||||
|
if (pi.status === PeerPullDebitRecordStatus.PendingDeposit) {
|
||||||
|
pi.status = PeerPullDebitRecordStatus.DonePaid;
|
||||||
|
}
|
||||||
|
await tx.peerPullPaymentIncoming.put(pi);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Finished,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function suspendPeerPushCreditTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPushPaymentIncomingId: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pushCreditRec = await tx.peerPushPaymentIncoming.get(
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!pushCreditRec) {
|
||||||
|
logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined;
|
||||||
|
switch (pushCreditRec.status) {
|
||||||
|
case PeerPushPaymentIncomingStatus.DialogProposed:
|
||||||
|
case PeerPushPaymentIncomingStatus.Done:
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMerge:
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMerge:
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.SuspendedMerge;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingWithdrawing:
|
||||||
|
// FIXME: Suspend internal withdrawal transaction!
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.SuspendedWithdrawing;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.Aborted:
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.Failed:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pushCreditRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
|
||||||
|
pushCreditRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
|
||||||
|
await tx.peerPushPaymentIncoming.put(pushCreditRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function abortPeerPushCreditTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPushPaymentIncomingId: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pushCreditRec = await tx.peerPushPaymentIncoming.get(
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!pushCreditRec) {
|
||||||
|
logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined;
|
||||||
|
switch (pushCreditRec.status) {
|
||||||
|
case PeerPushPaymentIncomingStatus.DialogProposed:
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.Aborted;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.Done:
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMerge:
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.Aborted;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.Aborted;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMerge:
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.Aborted;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingWithdrawing:
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.Aborted;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.Aborted:
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.Failed:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pushCreditRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
|
||||||
|
pushCreditRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
|
||||||
|
await tx.peerPushPaymentIncoming.put(pushCreditRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function failPeerPushCreditTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPushPaymentIncomingId: string,
|
||||||
|
) {
|
||||||
|
// We don't have any "aborting" states!
|
||||||
|
throw Error("can't run cancel-aborting on peer-push-credit transaction");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resumePeerPushCreditTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPushPaymentIncomingId: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushCredit,
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pushCreditRec = await tx.peerPushPaymentIncoming.get(
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!pushCreditRec) {
|
||||||
|
logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined;
|
||||||
|
switch (pushCreditRec.status) {
|
||||||
|
case PeerPushPaymentIncomingStatus.DialogProposed:
|
||||||
|
case PeerPushPaymentIncomingStatus.Done:
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMerge:
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingWithdrawing:
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMerge:
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.PendingMerge;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.PendingMergeKycRequired;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
|
||||||
|
// FIXME: resume underlying "internal-withdrawal" transaction.
|
||||||
|
newStatus = PeerPushPaymentIncomingStatus.PendingWithdrawing;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.Aborted:
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentIncomingStatus.Failed:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pushCreditRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
|
||||||
|
pushCreditRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
|
||||||
|
await tx.peerPushPaymentIncoming.put(pushCreditRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
ws.workAvailable.trigger();
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computePeerPushCreditTransactionState(
|
||||||
|
pushCreditRecord: PeerPushPaymentIncomingRecord,
|
||||||
|
): TransactionState {
|
||||||
|
switch (pushCreditRecord.status) {
|
||||||
|
case PeerPushPaymentIncomingStatus.DialogProposed:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Dialog,
|
||||||
|
minor: TransactionMinorState.Proposed,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMerge:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.Merge,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentIncomingStatus.Done:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Done,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.KycRequired,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingWithdrawing:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.Withdraw,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMerge:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
minor: TransactionMinorState.Merge,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
minor: TransactionMinorState.MergeKycRequired,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
minor: TransactionMinorState.Withdraw,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentIncomingStatus.Aborted:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Aborted,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentIncomingStatus.Failed:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Failed,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
assertUnreachable(pushCreditRecord.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computePeerPushCreditTransactionActions(
|
||||||
|
pushCreditRecord: PeerPushPaymentIncomingRecord,
|
||||||
|
): TransactionAction[] {
|
||||||
|
switch (pushCreditRecord.status) {
|
||||||
|
case PeerPushPaymentIncomingStatus.DialogProposed:
|
||||||
|
return [];
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMerge:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||||
|
case PeerPushPaymentIncomingStatus.Done:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||||
|
case PeerPushPaymentIncomingStatus.PendingWithdrawing:
|
||||||
|
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMerge:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||||
|
case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||||
|
case PeerPushPaymentIncomingStatus.Aborted:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPushPaymentIncomingStatus.Failed:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
default:
|
||||||
|
assertUnreachable(pushCreditRecord.status);
|
||||||
|
}
|
||||||
|
}
|
742
packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
Normal file
742
packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
Normal file
@ -0,0 +1,742 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022-2023 Taler Systems S.A.
|
||||||
|
|
||||||
|
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/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Amounts,
|
||||||
|
CheckPeerPushDebitRequest,
|
||||||
|
CheckPeerPushDebitResponse,
|
||||||
|
ContractTermsUtil,
|
||||||
|
HttpStatusCode,
|
||||||
|
InitiatePeerPushDebitRequest,
|
||||||
|
InitiatePeerPushDebitResponse,
|
||||||
|
Logger,
|
||||||
|
RefreshReason,
|
||||||
|
TalerError,
|
||||||
|
TalerErrorCode,
|
||||||
|
TalerPreciseTimestamp,
|
||||||
|
TransactionAction,
|
||||||
|
TransactionMajorState,
|
||||||
|
TransactionMinorState,
|
||||||
|
TransactionState,
|
||||||
|
TransactionType,
|
||||||
|
constructPayPushUri,
|
||||||
|
j2s,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
|
import {
|
||||||
|
selectPeerCoins,
|
||||||
|
getTotalPeerPaymentCost,
|
||||||
|
codecForExchangePurseStatus,
|
||||||
|
queryCoinInfosForSelection,
|
||||||
|
} from "./pay-peer-common.js";
|
||||||
|
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
|
||||||
|
import {
|
||||||
|
PeerPushPaymentInitiationRecord,
|
||||||
|
PeerPushPaymentInitiationStatus,
|
||||||
|
} from "../index.js";
|
||||||
|
import { PendingTaskType } from "../pending-types.js";
|
||||||
|
import {
|
||||||
|
OperationAttemptResult,
|
||||||
|
OperationAttemptResultType,
|
||||||
|
constructTaskIdentifier,
|
||||||
|
} from "../util/retries.js";
|
||||||
|
import {
|
||||||
|
runLongpollAsync,
|
||||||
|
spendCoins,
|
||||||
|
runOperationWithErrorReporting,
|
||||||
|
} from "./common.js";
|
||||||
|
import {
|
||||||
|
constructTransactionIdentifier,
|
||||||
|
notifyTransition,
|
||||||
|
stopLongpolling,
|
||||||
|
} from "./transactions.js";
|
||||||
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
|
|
||||||
|
const logger = new Logger("pay-peer-push-debit.ts");
|
||||||
|
|
||||||
|
export async function checkPeerPushDebit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: CheckPeerPushDebitRequest,
|
||||||
|
): Promise<CheckPeerPushDebitResponse> {
|
||||||
|
const instructedAmount = Amounts.parseOrThrow(req.amount);
|
||||||
|
const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
|
||||||
|
if (coinSelRes.type === "failure") {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
|
||||||
|
{
|
||||||
|
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const totalAmount = await getTotalPeerPaymentCost(
|
||||||
|
ws,
|
||||||
|
coinSelRes.result.coins,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
amountEffective: Amounts.stringify(totalAmount),
|
||||||
|
amountRaw: req.amount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processPeerPushDebitCreateReserve(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPushInitiation: PeerPushPaymentInitiationRecord,
|
||||||
|
): Promise<OperationAttemptResult> {
|
||||||
|
const pursePub = peerPushInitiation.pursePub;
|
||||||
|
const purseExpiration = peerPushInitiation.purseExpiration;
|
||||||
|
const hContractTerms = peerPushInitiation.contractTermsHash;
|
||||||
|
|
||||||
|
const purseSigResp = await ws.cryptoApi.signPurseCreation({
|
||||||
|
hContractTerms,
|
||||||
|
mergePub: peerPushInitiation.mergePub,
|
||||||
|
minAge: 0,
|
||||||
|
purseAmount: peerPushInitiation.amount,
|
||||||
|
purseExpiration,
|
||||||
|
pursePriv: peerPushInitiation.pursePriv,
|
||||||
|
});
|
||||||
|
|
||||||
|
const coins = await queryCoinInfosForSelection(
|
||||||
|
ws,
|
||||||
|
peerPushInitiation.coinSel,
|
||||||
|
);
|
||||||
|
|
||||||
|
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
|
||||||
|
exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
|
||||||
|
pursePub: peerPushInitiation.pursePub,
|
||||||
|
coins,
|
||||||
|
});
|
||||||
|
|
||||||
|
const econtractResp = await ws.cryptoApi.encryptContractForMerge({
|
||||||
|
contractTerms: peerPushInitiation.contractTerms,
|
||||||
|
mergePriv: peerPushInitiation.mergePriv,
|
||||||
|
pursePriv: peerPushInitiation.pursePriv,
|
||||||
|
pursePub: peerPushInitiation.pursePub,
|
||||||
|
contractPriv: peerPushInitiation.contractPriv,
|
||||||
|
contractPub: peerPushInitiation.contractPub,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPurseUrl = new URL(
|
||||||
|
`purses/${peerPushInitiation.pursePub}/create`,
|
||||||
|
peerPushInitiation.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const httpResp = await ws.http.fetch(createPurseUrl.href, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
amount: peerPushInitiation.amount,
|
||||||
|
merge_pub: peerPushInitiation.mergePub,
|
||||||
|
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 !== HttpStatusCode.Ok) {
|
||||||
|
throw Error("got error response from exchange");
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const ppi = await tx.peerPushPaymentInitiations.get(pursePub);
|
||||||
|
if (!ppi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ppi.status = PeerPushPaymentInitiationStatus.Done;
|
||||||
|
await tx.peerPushPaymentInitiations.put(ppi);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Finished,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transitionPeerPushDebitFromReadyToDone(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub);
|
||||||
|
if (!ppiRec) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (ppiRec.status !== PeerPushPaymentInitiationStatus.PendingReady) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const oldTxState = computePeerPushDebitTransactionState(ppiRec);
|
||||||
|
ppiRec.status = PeerPushPaymentInitiationStatus.Done;
|
||||||
|
const newTxState = computePeerPushDebitTransactionState(ppiRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the "pending(ready)" state of a peer-push-debit transaction.
|
||||||
|
*/
|
||||||
|
async function processPeerPushDebitReady(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
peerPushInitiation: PeerPushPaymentInitiationRecord,
|
||||||
|
): Promise<OperationAttemptResult> {
|
||||||
|
const pursePub = peerPushInitiation.pursePub;
|
||||||
|
const retryTag = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
runLongpollAsync(ws, retryTag, async (ct) => {
|
||||||
|
const mergeUrl = new URL(`purses/${pursePub}/merge`);
|
||||||
|
mergeUrl.searchParams.set("timeout_ms", "30000");
|
||||||
|
const resp = await ws.http.fetch(mergeUrl.href, {
|
||||||
|
// timeout: getReserveRequestTimeout(withdrawalGroup),
|
||||||
|
cancellationToken: ct,
|
||||||
|
});
|
||||||
|
if (resp.status === HttpStatusCode.Ok) {
|
||||||
|
const purseStatus = await readSuccessResponseJsonOrThrow(
|
||||||
|
resp,
|
||||||
|
codecForExchangePurseStatus(),
|
||||||
|
);
|
||||||
|
if (purseStatus.deposit_timestamp) {
|
||||||
|
await transitionPeerPushDebitFromReadyToDone(
|
||||||
|
ws,
|
||||||
|
peerPushInitiation.pursePub,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (resp.status === HttpStatusCode.Gone) {
|
||||||
|
// FIXME: transition the reserve into the expired state
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
logger.trace(
|
||||||
|
"returning early from peer-push-debit for long-polling in background",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Longpoll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processPeerPushDebit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
): Promise<OperationAttemptResult> {
|
||||||
|
const peerPushInitiation = await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentInitiations])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.peerPushPaymentInitiations.get(pursePub);
|
||||||
|
});
|
||||||
|
if (!peerPushInitiation) {
|
||||||
|
throw Error("peer push payment not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryTag = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
|
||||||
|
// We're already running!
|
||||||
|
if (ws.activeLongpoll[retryTag]) {
|
||||||
|
logger.info("peer-push-debit task already in long-polling, returning!");
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Longpoll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (peerPushInitiation.status) {
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
return processPeerPushDebitCreateReserve(ws, peerPushInitiation);
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingReady:
|
||||||
|
return processPeerPushDebitReady(ws, peerPushInitiation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Finished,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate sending a peer-to-peer push payment.
|
||||||
|
*/
|
||||||
|
export async function initiatePeerPushDebit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: InitiatePeerPushDebitRequest,
|
||||||
|
): Promise<InitiatePeerPushDebitResponse> {
|
||||||
|
const instructedAmount = Amounts.parseOrThrow(
|
||||||
|
req.partialContractTerms.amount,
|
||||||
|
);
|
||||||
|
const purseExpiration = req.partialContractTerms.purse_expiration;
|
||||||
|
const contractTerms = req.partialContractTerms;
|
||||||
|
|
||||||
|
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
|
||||||
|
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
|
||||||
|
|
||||||
|
const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
|
||||||
|
const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
|
||||||
|
|
||||||
|
if (coinSelRes.type !== "success") {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
|
||||||
|
{
|
||||||
|
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sel = coinSelRes.result;
|
||||||
|
|
||||||
|
logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
|
||||||
|
|
||||||
|
const totalAmount = await getTotalPeerPaymentCost(
|
||||||
|
ws,
|
||||||
|
coinSelRes.result.coins,
|
||||||
|
);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [
|
||||||
|
x.exchanges,
|
||||||
|
x.contractTerms,
|
||||||
|
x.coins,
|
||||||
|
x.coinAvailability,
|
||||||
|
x.denominations,
|
||||||
|
x.refreshGroups,
|
||||||
|
x.peerPushPaymentInitiations,
|
||||||
|
])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
// FIXME: Instead of directly doing a spendCoin here,
|
||||||
|
// we might want to mark the coins as used and spend them
|
||||||
|
// after we've been able to create the purse.
|
||||||
|
await spendCoins(ws, tx, {
|
||||||
|
// allocationId: `txn:peer-push-debit:${pursePair.pub}`,
|
||||||
|
allocationId: constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushDebit,
|
||||||
|
pursePub: 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: contractKeyPair.priv,
|
||||||
|
contractPub: contractKeyPair.pub,
|
||||||
|
contractTermsHash: hContractTerms,
|
||||||
|
exchangeBaseUrl: sel.exchangeBaseUrl,
|
||||||
|
mergePriv: mergePair.priv,
|
||||||
|
mergePub: mergePair.pub,
|
||||||
|
purseExpiration: purseExpiration,
|
||||||
|
pursePriv: pursePair.priv,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
timestampCreated: TalerPreciseTimestamp.now(),
|
||||||
|
status: PeerPushPaymentInitiationStatus.PendingCreatePurse,
|
||||||
|
contractTerms: contractTerms,
|
||||||
|
coinSel: {
|
||||||
|
coinPubs: sel.coins.map((x) => x.coinPub),
|
||||||
|
contributions: sel.coins.map((x) => x.contribution),
|
||||||
|
},
|
||||||
|
totalCost: Amounts.stringify(totalAmount),
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.contractTerms.put({
|
||||||
|
h: hContractTerms,
|
||||||
|
contractTermsRaw: contractTerms,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushDebit,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runOperationWithErrorReporting(ws, taskId, async () => {
|
||||||
|
return await processPeerPushDebit(ws, pursePair.pub);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
contractPriv: contractKeyPair.priv,
|
||||||
|
mergePriv: mergePair.priv,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
|
||||||
|
talerUri: constructPayPushUri({
|
||||||
|
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
|
||||||
|
contractPriv: contractKeyPair.priv,
|
||||||
|
}),
|
||||||
|
transactionId: constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushDebit,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computePeerPushDebitTransactionActions(
|
||||||
|
ppiRecord: PeerPushPaymentInitiationRecord,
|
||||||
|
): TransactionAction[] {
|
||||||
|
switch (ppiRecord.status) {
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingReady:
|
||||||
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||||
|
case PeerPushPaymentInitiationStatus.Aborted:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingRefresh:
|
||||||
|
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedReady:
|
||||||
|
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||||
|
case PeerPushPaymentInitiationStatus.Done:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
case PeerPushPaymentInitiationStatus.Failed:
|
||||||
|
return [TransactionAction.Delete];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function abortPeerPushDebitTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
|
||||||
|
if (!pushDebitRec) {
|
||||||
|
logger.warn(`peer push debit ${pursePub} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
|
||||||
|
switch (pushDebitRec.status) {
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingReady:
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedReady:
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
// Network request might already be in-flight!
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingRefresh:
|
||||||
|
case PeerPushPaymentInitiationStatus.Done:
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.Aborted:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.Failed:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pushDebitRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
|
||||||
|
pushDebitRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
|
||||||
|
await tx.peerPushPaymentInitiations.put(pushDebitRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function failPeerPushDebitTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
|
||||||
|
if (!pushDebitRec) {
|
||||||
|
logger.warn(`peer push debit ${pursePub} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
|
||||||
|
switch (pushDebitRec.status) {
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingRefresh:
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
|
||||||
|
// FIXME: We also need to abort the refresh group!
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.Aborted;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.Aborted;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingReady:
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedReady:
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.Done:
|
||||||
|
case PeerPushPaymentInitiationStatus.Aborted:
|
||||||
|
case PeerPushPaymentInitiationStatus.Failed:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pushDebitRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
|
||||||
|
pushDebitRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
|
||||||
|
await tx.peerPushPaymentInitiations.put(pushDebitRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function suspendPeerPushDebitTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
|
||||||
|
if (!pushDebitRec) {
|
||||||
|
logger.warn(`peer push debit ${pursePub} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
|
||||||
|
switch (pushDebitRec.status) {
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.SuspendedCreatePurse;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingRefresh:
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
newStatus =
|
||||||
|
PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingReady:
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.SuspendedReady;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedReady:
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.Done:
|
||||||
|
case PeerPushPaymentInitiationStatus.Aborted:
|
||||||
|
case PeerPushPaymentInitiationStatus.Failed:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pushDebitRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
|
||||||
|
pushDebitRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
|
||||||
|
await tx.peerPushPaymentInitiations.put(pushDebitRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resumePeerPushDebitTransaction(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
pursePub: string,
|
||||||
|
) {
|
||||||
|
const taskId = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.PeerPushDebit,
|
||||||
|
pursePub,
|
||||||
|
});
|
||||||
|
stopLongpolling(ws, taskId);
|
||||||
|
const transitionInfo = await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
|
||||||
|
if (!pushDebitRec) {
|
||||||
|
logger.warn(`peer push debit ${pursePub} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
|
||||||
|
switch (pushDebitRec.status) {
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.AbortingRefresh;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedReady:
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.PendingReady;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
newStatus = PeerPushPaymentInitiationStatus.PendingCreatePurse;
|
||||||
|
break;
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingRefresh:
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingReady:
|
||||||
|
case PeerPushPaymentInitiationStatus.Done:
|
||||||
|
case PeerPushPaymentInitiationStatus.Aborted:
|
||||||
|
case PeerPushPaymentInitiationStatus.Failed:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(pushDebitRec.status);
|
||||||
|
}
|
||||||
|
if (newStatus != null) {
|
||||||
|
const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
|
||||||
|
pushDebitRec.status = newStatus;
|
||||||
|
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
|
||||||
|
await tx.peerPushPaymentInitiations.put(pushDebitRec);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
ws.workAvailable.trigger();
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function computePeerPushDebitTransactionState(
|
||||||
|
ppiRecord: PeerPushPaymentInitiationRecord,
|
||||||
|
): TransactionState {
|
||||||
|
switch (ppiRecord.status) {
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.CreatePurse,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.PendingReady:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
minor: TransactionMinorState.Ready,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.Aborted:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Aborted,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Aborting,
|
||||||
|
minor: TransactionMinorState.DeletePurse,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.AbortingRefresh:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Aborting,
|
||||||
|
minor: TransactionMinorState.Refresh,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.SuspendedAborting,
|
||||||
|
minor: TransactionMinorState.DeletePurse,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.SuspendedAborting,
|
||||||
|
minor: TransactionMinorState.Refresh,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
minor: TransactionMinorState.CreatePurse,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.SuspendedReady:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
minor: TransactionMinorState.Ready,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.Done:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Done,
|
||||||
|
};
|
||||||
|
case PeerPushPaymentInitiationStatus.Failed:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Failed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -50,14 +50,10 @@ import { getBalances } from "./balance.js";
|
|||||||
import { checkLogicInvariant } from "../util/invariants.js";
|
import { checkLogicInvariant } from "../util/invariants.js";
|
||||||
import { acceptWithdrawalFromUri } from "./withdraw.js";
|
import { acceptWithdrawalFromUri } from "./withdraw.js";
|
||||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||||
import {
|
import { initiatePeerPullPayment } from "./pay-peer-pull-credit.js";
|
||||||
confirmPeerPullDebit,
|
import { preparePeerPullDebit, confirmPeerPullDebit } from "./pay-peer-pull-debit.js";
|
||||||
confirmPeerPushCredit,
|
import { preparePeerPushCredit, confirmPeerPushCredit } from "./pay-peer-push-credit.js";
|
||||||
initiatePeerPullPayment,
|
import { initiatePeerPushDebit } from "./pay-peer-push-debit.js";
|
||||||
initiatePeerPushDebit,
|
|
||||||
preparePeerPullDebit,
|
|
||||||
preparePeerPushCredit,
|
|
||||||
} from "./pay-peer.js";
|
|
||||||
|
|
||||||
const logger = new Logger("operations/testing.ts");
|
const logger = new Logger("operations/testing.ts");
|
||||||
|
|
||||||
|
@ -92,32 +92,6 @@ import {
|
|||||||
suspendPayMerchant,
|
suspendPayMerchant,
|
||||||
computePayMerchantTransactionActions,
|
computePayMerchantTransactionActions,
|
||||||
} from "./pay-merchant.js";
|
} from "./pay-merchant.js";
|
||||||
import {
|
|
||||||
abortPeerPullCreditTransaction,
|
|
||||||
abortPeerPullDebitTransaction,
|
|
||||||
abortPeerPushCreditTransaction,
|
|
||||||
abortPeerPushDebitTransaction,
|
|
||||||
failPeerPullCreditTransaction,
|
|
||||||
failPeerPullDebitTransaction,
|
|
||||||
failPeerPushCreditTransaction,
|
|
||||||
failPeerPushDebitTransaction,
|
|
||||||
computePeerPullCreditTransactionState,
|
|
||||||
computePeerPullDebitTransactionState,
|
|
||||||
computePeerPushCreditTransactionState,
|
|
||||||
computePeerPushDebitTransactionState,
|
|
||||||
resumePeerPullCreditTransaction,
|
|
||||||
resumePeerPullDebitTransaction,
|
|
||||||
resumePeerPushCreditTransaction,
|
|
||||||
resumePeerPushDebitTransaction,
|
|
||||||
suspendPeerPullCreditTransaction,
|
|
||||||
suspendPeerPullDebitTransaction,
|
|
||||||
suspendPeerPushCreditTransaction,
|
|
||||||
suspendPeerPushDebitTransaction,
|
|
||||||
computePeerPushDebitTransactionActions,
|
|
||||||
computePeerPullDebitTransactionActions,
|
|
||||||
computePeerPullCreditTransactionActions,
|
|
||||||
computePeerPushCreditTransactionActions,
|
|
||||||
} from "./pay-peer.js";
|
|
||||||
import {
|
import {
|
||||||
abortRefreshGroup,
|
abortRefreshGroup,
|
||||||
failRefreshGroup,
|
failRefreshGroup,
|
||||||
@ -143,6 +117,10 @@ import {
|
|||||||
suspendWithdrawalTransaction,
|
suspendWithdrawalTransaction,
|
||||||
computeWithdrawalTransactionActions,
|
computeWithdrawalTransactionActions,
|
||||||
} from "./withdraw.js";
|
} from "./withdraw.js";
|
||||||
|
import { computePeerPullCreditTransactionState, computePeerPullCreditTransactionActions, suspendPeerPullCreditTransaction, failPeerPullCreditTransaction, resumePeerPullCreditTransaction, abortPeerPullCreditTransaction } from "./pay-peer-pull-credit.js";
|
||||||
|
import { computePeerPullDebitTransactionState, computePeerPullDebitTransactionActions, suspendPeerPullDebitTransaction, failPeerPullDebitTransaction, resumePeerPullDebitTransaction, abortPeerPullDebitTransaction } from "./pay-peer-pull-debit.js";
|
||||||
|
import { computePeerPushCreditTransactionState, computePeerPushCreditTransactionActions, suspendPeerPushCreditTransaction, failPeerPushCreditTransaction, resumePeerPushCreditTransaction, abortPeerPushCreditTransaction } from "./pay-peer-push-credit.js";
|
||||||
|
import { computePeerPushDebitTransactionState, computePeerPushDebitTransactionActions, suspendPeerPushDebitTransaction, failPeerPushDebitTransaction, resumePeerPushDebitTransaction, abortPeerPushDebitTransaction } from "./pay-peer-push-debit.js";
|
||||||
|
|
||||||
const logger = new Logger("taler-wallet-core:transactions.ts");
|
const logger = new Logger("taler-wallet-core:transactions.ts");
|
||||||
|
|
||||||
|
@ -63,8 +63,6 @@ import {
|
|||||||
codecForAddKnownBankAccounts,
|
codecForAddKnownBankAccounts,
|
||||||
codecForAny,
|
codecForAny,
|
||||||
codecForApplyDevExperiment,
|
codecForApplyDevExperiment,
|
||||||
codecForApplyRefundFromPurchaseIdRequest,
|
|
||||||
codecForApplyRefundRequest,
|
|
||||||
codecForCancelAbortingTransactionRequest,
|
codecForCancelAbortingTransactionRequest,
|
||||||
codecForCheckPeerPullPaymentRequest,
|
codecForCheckPeerPullPaymentRequest,
|
||||||
codecForCheckPeerPushDebitRequest,
|
codecForCheckPeerPushDebitRequest,
|
||||||
@ -196,22 +194,29 @@ import {
|
|||||||
getContractTermsDetails,
|
getContractTermsDetails,
|
||||||
preparePayForUri,
|
preparePayForUri,
|
||||||
processPurchase,
|
processPurchase,
|
||||||
|
startQueryRefund,
|
||||||
startRefundQueryForUri,
|
startRefundQueryForUri,
|
||||||
} from "./operations/pay-merchant.js";
|
} from "./operations/pay-merchant.js";
|
||||||
import {
|
import {
|
||||||
checkPeerPullPaymentInitiation,
|
checkPeerPullPaymentInitiation,
|
||||||
checkPeerPushDebit,
|
|
||||||
confirmPeerPullDebit,
|
|
||||||
confirmPeerPushCredit,
|
|
||||||
initiatePeerPullPayment,
|
initiatePeerPullPayment,
|
||||||
initiatePeerPushDebit,
|
|
||||||
preparePeerPullDebit,
|
|
||||||
preparePeerPushCredit,
|
|
||||||
processPeerPullCredit,
|
processPeerPullCredit,
|
||||||
|
} from "./operations/pay-peer-pull-credit.js";
|
||||||
|
import {
|
||||||
|
confirmPeerPullDebit,
|
||||||
|
preparePeerPullDebit,
|
||||||
|
} from "./operations/pay-peer-pull-debit.js";
|
||||||
|
import {
|
||||||
|
confirmPeerPushCredit,
|
||||||
|
preparePeerPushCredit,
|
||||||
processPeerPullDebit,
|
processPeerPullDebit,
|
||||||
processPeerPushCredit,
|
processPeerPushCredit,
|
||||||
|
} from "./operations/pay-peer-push-credit.js";
|
||||||
|
import {
|
||||||
|
checkPeerPushDebit,
|
||||||
|
initiatePeerPushDebit,
|
||||||
processPeerPushDebit,
|
processPeerPushDebit,
|
||||||
} from "./operations/pay-peer.js";
|
} from "./operations/pay-peer-push-debit.js";
|
||||||
import { getPendingOperations } from "./operations/pending.js";
|
import { getPendingOperations } from "./operations/pending.js";
|
||||||
import {
|
import {
|
||||||
createRecoupGroup,
|
createRecoupGroup,
|
||||||
@ -232,8 +237,8 @@ import {
|
|||||||
import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
|
import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
|
||||||
import {
|
import {
|
||||||
abortTransaction,
|
abortTransaction,
|
||||||
failTransaction,
|
|
||||||
deleteTransaction,
|
deleteTransaction,
|
||||||
|
failTransaction,
|
||||||
getTransactionById,
|
getTransactionById,
|
||||||
getTransactions,
|
getTransactions,
|
||||||
parseTransactionIdentifier,
|
parseTransactionIdentifier,
|
||||||
@ -280,7 +285,6 @@ import {
|
|||||||
WalletCoreApiClient,
|
WalletCoreApiClient,
|
||||||
WalletCoreResponseType,
|
WalletCoreResponseType,
|
||||||
} from "./wallet-api-types.js";
|
} from "./wallet-api-types.js";
|
||||||
import { startQueryRefund } from "./operations/pay-merchant.js";
|
|
||||||
|
|
||||||
const logger = new Logger("wallet.ts");
|
const logger = new Logger("wallet.ts");
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user