/*
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
*/
import {
AcceptPeerPullPaymentResponse,
Amounts,
CoinRefreshRequest,
ConfirmPeerPullDebitRequest,
ContractTermsUtil,
ExchangePurseDeposits,
HttpStatusCode,
Logger,
NotificationType,
PeerContractTerms,
PreparePeerPullDebitRequest,
PreparePeerPullDebitResponse,
RefreshReason,
TalerError,
TalerErrorCode,
TalerPreciseTimestamp,
TalerProtocolViolationError,
TransactionAction,
TransactionMajorState,
TransactionMinorState,
TransactionState,
TransactionType,
codecForAny,
codecForExchangeGetContractResponse,
codecForPeerContractTerms,
decodeCrock,
eddsaGetPublic,
encodeCrock,
getRandomBytes,
j2s,
parsePayPullUri,
} from "@gnu-taler/taler-util";
import {
HttpResponse,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import {
InternalWalletState,
PeerPullDebitRecordStatus,
PeerPullPaymentIncomingRecord,
PendingTaskType,
RefreshOperationStatus,
createRefreshGroup,
timestampPreciseToDb,
} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
import {
TaskRunResult,
TaskRunResultType,
constructTaskIdentifier,
spendCoins,
} from "./common.js";
import {
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
queryCoinInfosForSelection,
} from "./pay-peer-common.js";
import {
constructTransactionIdentifier,
notifyTransition,
parseTransactionIdentifier,
stopLongpolling,
} from "./transactions.js";
import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
const logger = new Logger("pay-peer-pull-debit.ts");
async function handlePurseCreationConflict(
ws: InternalWalletState,
peerPullInc: PeerPullPaymentIncomingRecord,
resp: HttpResponse,
): Promise {
const pursePub = peerPullInc.pursePub;
const errResp = await readTalerErrorResponse(resp);
if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
await failPeerPullDebitTransaction(ws, pursePub);
return TaskRunResult.finished();
}
// FIXME: Properly parse!
const brokenCoinPub = (errResp as any).coin_pub;
logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
if (!brokenCoinPub) {
// FIXME: Details!
throw new TalerProtocolViolationError();
}
const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
const sel = peerPullInc.coinSel;
if (!sel) {
throw Error("invalid state (coin selection expected)");
}
const repair: PeerCoinRepair = {
coinPubs: [],
contribs: [],
exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
};
for (let i = 0; i < sel.coinPubs.length; i++) {
if (sel.coinPubs[i] != brokenCoinPub) {
repair.coinPubs.push(sel.coinPubs[i]);
repair.contribs.push(Amounts.parseOrThrow(sel.contributions[i]));
}
}
const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair });
if (coinSelRes.type == "failure") {
// FIXME: Details!
throw Error(
"insufficient balance to re-select coins to repair double spending",
);
}
const totalAmount = await getTotalPeerPaymentCost(
ws,
coinSelRes.result.coins,
);
await ws.db
.mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => {
const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
if (!myPpi) {
return;
}
switch (myPpi.status) {
case PeerPullDebitRecordStatus.PendingDeposit:
case PeerPullDebitRecordStatus.SuspendedDeposit: {
const sel = coinSelRes.result;
myPpi.coinSel = {
coinPubs: sel.coins.map((x) => x.coinPub),
contributions: sel.coins.map((x) => x.contribution),
totalCost: Amounts.stringify(totalAmount),
};
break;
}
default:
return;
}
await tx.peerPullDebit.put(myPpi);
});
return TaskRunResult.finished();
}
async function processPeerPullDebitPendingDeposit(
ws: InternalWalletState,
peerPullInc: PeerPullPaymentIncomingRecord,
): Promise {
const peerPullDebitId = peerPullInc.peerPullDebitId;
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 transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId,
});
const httpResp = await ws.http.fetch(purseDepositUrl.href, {
method: "POST",
body: depositPayload,
});
switch (httpResp.status) {
case HttpStatusCode.Ok: {
const resp = await readSuccessResponseJsonOrThrow(
httpResp,
codecForAny(),
);
logger.trace(`purse deposit response: ${j2s(resp)}`);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => {
const pi = await tx.peerPullDebit.get(peerPullDebitId);
if (!pi) {
throw Error("peer pull payment not found anymore");
}
if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
return;
}
const oldTxState = computePeerPullDebitTransactionState(pi);
pi.status = PeerPullDebitRecordStatus.Done;
const newTxState = computePeerPullDebitTransactionState(pi);
await tx.peerPullDebit.put(pi);
return { oldTxState, newTxState };
});
notifyTransition(ws, transactionId, transitionInfo);
break;
}
case HttpStatusCode.Gone: {
const transitionInfo = await ws.db
.mktx((x) => [
x.peerPullDebit,
x.refreshGroups,
x.denominations,
x.coinAvailability,
x.coins,
])
.runReadWrite(async (tx) => {
const pi = await tx.peerPullDebit.get(peerPullDebitId);
if (!pi) {
throw Error("peer pull payment not found anymore");
}
if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
return;
}
const oldTxState = computePeerPullDebitTransactionState(pi);
const currency = Amounts.currencyOf(pi.totalCostEstimated);
const coinPubs: CoinRefreshRequest[] = [];
if (!pi.coinSel) {
throw Error("invalid db state");
}
for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
coinPubs.push({
amount: pi.coinSel.contributions[i],
coinPub: pi.coinSel.coinPubs[i],
});
}
const refresh = await createRefreshGroup(
ws,
tx,
currency,
coinPubs,
RefreshReason.AbortPeerPushDebit,
);
pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
pi.abortRefreshGroupId = refresh.refreshGroupId;
const newTxState = computePeerPullDebitTransactionState(pi);
await tx.peerPullDebit.put(pi);
return { oldTxState, newTxState };
});
notifyTransition(ws, transactionId, transitionInfo);
break;
}
case HttpStatusCode.Conflict: {
return handlePurseCreationConflict(ws, peerPullInc, httpResp);
}
default: {
const errResp = await readTalerErrorResponse(httpResp);
return {
type: TaskRunResultType.Error,
errorDetail: errResp,
};
}
}
return TaskRunResult.finished();
}
async function processPeerPullDebitAbortingRefresh(
ws: InternalWalletState,
peerPullInc: PeerPullPaymentIncomingRecord,
): Promise {
const peerPullDebitId = peerPullInc.peerPullDebitId;
const abortRefreshGroupId = peerPullInc.abortRefreshGroupId;
checkLogicInvariant(!!abortRefreshGroupId);
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId,
});
const transitionInfo = await ws.db
.mktx((x) => [x.refreshGroups, x.peerPullDebit])
.runReadWrite(async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: PeerPullDebitRecordStatus | undefined;
if (!refreshGroup) {
// Maybe it got manually deleted? Means that we should
// just go into failed.
logger.warn("no aborting refresh group found for deposit group");
newOpState = PeerPullDebitRecordStatus.Failed;
} else {
if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
newOpState = PeerPullDebitRecordStatus.Aborted;
} else if (
refreshGroup.operationStatus === RefreshOperationStatus.Failed
) {
newOpState = PeerPullDebitRecordStatus.Failed;
}
}
if (newOpState) {
const newDg = await tx.peerPullDebit.get(peerPullDebitId);
if (!newDg) {
return;
}
const oldTxState = computePeerPullDebitTransactionState(newDg);
newDg.status = newOpState;
const newTxState = computePeerPullDebitTransactionState(newDg);
await tx.peerPullDebit.put(newDg);
return { oldTxState, newTxState };
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
// FIXME: Shouldn't this be finished in some cases?!
return TaskRunResult.pending();
}
export async function processPeerPullDebit(
ws: InternalWalletState,
peerPullDebitId: string,
): Promise {
const peerPullInc = await ws.db
.mktx((x) => [x.peerPullDebit])
.runReadOnly(async (tx) => {
return tx.peerPullDebit.get(peerPullDebitId);
});
if (!peerPullInc) {
throw Error("peer pull debit not found");
}
switch (peerPullInc.status) {
case PeerPullDebitRecordStatus.PendingDeposit:
return await processPeerPullDebitPendingDeposit(ws, peerPullInc);
case PeerPullDebitRecordStatus.AbortingRefresh:
return await processPeerPullDebitAbortingRefresh(ws, peerPullInc);
}
return TaskRunResult.finished();
}
export async function confirmPeerPullDebit(
ws: InternalWalletState,
req: ConfirmPeerPullDebitRequest,
): Promise {
let peerPullDebitId: string;
if (req.transactionId) {
const parsedTx = parseTransactionIdentifier(req.transactionId);
if (!parsedTx || parsedTx.tag !== TransactionType.PeerPullDebit) {
throw Error("invalid peer-pull-debit transaction identifier");
}
peerPullDebitId = parsedTx.peerPullDebitId;
} else if (req.peerPullDebitId) {
peerPullDebitId = req.peerPullDebitId;
} else {
throw Error("invalid request, transactionId or peerPullDebitId required");
}
const peerPullInc = await ws.db
.mktx((x) => [x.peerPullDebit])
.runReadOnly(async (tx) => {
return tx.peerPullDebit.get(peerPullDebitId);
});
if (!peerPullInc) {
throw Error(
`can't accept unknown incoming p2p pull payment (${req.peerPullDebitId})`,
);
}
const instructedAmount = Amounts.parseOrThrow(peerPullInc.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.peerPullDebit,
x.coinAvailability,
])
.runReadWrite(async (tx) => {
await spendCoins(ws, tx, {
// allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
allocationId: constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId,
}),
coinPubs: sel.coins.map((x) => x.coinPub),
contributions: sel.coins.map((x) =>
Amounts.parseOrThrow(x.contribution),
),
refreshReason: RefreshReason.PayPeerPull,
});
const pi = await tx.peerPullDebit.get(peerPullDebitId);
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.peerPullDebit.put(pi);
return pi;
});
ws.notify({ type: NotificationType.BalanceChange });
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId,
});
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 {
const uri = parsePayPullUri(req.talerUri);
if (!uri) {
throw Error("got invalid taler://pay-pull URI");
}
const existing = await ws.db
.mktx((x) => [x.peerPullDebit, x.contractTerms])
.runReadOnly(async (tx) => {
const peerPullDebitRecord =
await tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([
uri.exchangeBaseUrl,
uri.contractPriv,
]);
if (!peerPullDebitRecord) {
return;
}
const contractTerms = await tx.contractTerms.get(
peerPullDebitRecord.contractTermsHash,
);
if (!contractTerms) {
return;
}
return { peerPullDebitRecord, contractTerms };
});
if (existing) {
return {
amount: existing.peerPullDebitRecord.amount,
amountRaw: existing.peerPullDebitRecord.amount,
amountEffective: existing.peerPullDebitRecord.totalCostEstimated,
contractTerms: existing.contractTerms.contractTermsRaw,
peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
}),
};
}
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.fetch(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.fetch(getPurseUrl.href);
const purseStatus = await readSuccessResponseJsonOrThrow(
purseHttpResp,
codecForExchangePurseStatus(),
);
const peerPullDebitId = 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");
}
const contractTermsHash = ContractTermsUtil.hashContractTerms(contractTerms);
// 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.peerPullDebit, x.contractTerms])
.runReadWrite(async (tx) => {
await tx.contractTerms.put({
h: contractTermsHash,
contractTermsRaw: contractTerms,
}),
await tx.peerPullDebit.add({
peerPullDebitId,
contractPriv: contractPriv,
exchangeBaseUrl: exchangeBaseUrl,
pursePub: pursePub,
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
contractTermsHash,
amount: contractTerms.amount,
status: PeerPullDebitRecordStatus.DialogProposed,
totalCostEstimated: Amounts.stringify(totalAmount),
});
});
return {
amount: contractTerms.amount,
amountEffective: Amounts.stringify(totalAmount),
amountRaw: contractTerms.amount,
contractTerms: contractTerms,
peerPullDebitId,
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId: peerPullDebitId,
}),
};
}
export async function suspendPeerPullDebitTransaction(
ws: InternalWalletState,
peerPullDebitId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPullDebit,
peerPullDebitId,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
if (!pullDebitRec) {
logger.warn(`peer pull debit ${peerPullDebitId} not found`);
return;
}
let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
switch (pullDebitRec.status) {
case PeerPullDebitRecordStatus.DialogProposed:
break;
case PeerPullDebitRecordStatus.Done:
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.peerPullDebit.put(pullDebitRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
}
export async function abortPeerPullDebitTransaction(
ws: InternalWalletState,
peerPullDebitId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPullDebit,
peerPullDebitId,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
if (!pullDebitRec) {
logger.warn(`peer pull debit ${peerPullDebitId} not found`);
return;
}
let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
switch (pullDebitRec.status) {
case PeerPullDebitRecordStatus.DialogProposed:
newStatus = PeerPullDebitRecordStatus.Aborted;
break;
case PeerPullDebitRecordStatus.Done:
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.peerPullDebit.put(pullDebitRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
}
export async function failPeerPullDebitTransaction(
ws: InternalWalletState,
peerPullDebitId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPullDebit,
peerPullDebitId,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
if (!pullDebitRec) {
logger.warn(`peer pull debit ${peerPullDebitId} not found`);
return;
}
let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
switch (pullDebitRec.status) {
case PeerPullDebitRecordStatus.DialogProposed:
newStatus = PeerPullDebitRecordStatus.Aborted;
break;
case PeerPullDebitRecordStatus.Done:
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.peerPullDebit.put(pullDebitRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
}
export async function resumePeerPullDebitTransaction(
ws: InternalWalletState,
peerPullDebitId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPullDebit,
peerPullDebitId,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
if (!pullDebitRec) {
logger.warn(`peer pull debit ${peerPullDebitId} not found`);
return;
}
let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
switch (pullDebitRec.status) {
case PeerPullDebitRecordStatus.DialogProposed:
case PeerPullDebitRecordStatus.Done:
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.peerPullDebit.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.Done:
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.Done:
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];
}
}