/*
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 {
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 {
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 {
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 {
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 {
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);
}
}