wallet-core/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts

934 lines
29 KiB
TypeScript

/*
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 {
AcceptPeerPushPaymentResponse,
Amounts,
ConfirmPeerPushCreditRequest,
ContractTermsUtil,
ExchangePurseMergeRequest,
HttpStatusCode,
Logger,
NotificationType,
PeerContractTerms,
PreparePeerPushCreditRequest,
PreparePeerPushCreditResponse,
TalerErrorCode,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
TransactionAction,
TransactionMajorState,
TransactionMinorState,
TransactionState,
TransactionType,
WalletAccountMergeFlags,
WalletKycUuid,
codecForAny,
codecForExchangeGetContractResponse,
codecForPeerContractTerms,
codecForWalletKycUuid,
decodeCrock,
eddsaGetPublic,
encodeCrock,
getRandomBytes,
j2s,
makeErrorDetail,
parsePayPushUri,
talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import {
InternalWalletState,
KycPendingInfo,
KycUserType,
PeerPushPaymentIncomingRecord,
PeerPushCreditStatus,
PendingTaskType,
WithdrawalGroupStatus,
WithdrawalRecordType,
timestampPreciseToDb,
} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant } from "../util/invariants.js";
import {
TaskRunResult,
TaskRunResultType,
constructTaskIdentifier,
runLongpollAsync,
} from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import {
codecForExchangePurseStatus,
getMergeReserveInfo,
} from "./pay-peer-common.js";
import {
TransitionInfo,
constructTransactionIdentifier,
notifyTransition,
parseTransactionIdentifier,
stopLongpolling,
} from "./transactions.js";
import {
getExchangeWithdrawalInfo,
internalPerformCreateWithdrawalGroup,
internalPrepareCreateWithdrawalGroup,
} from "./withdraw.js";
const logger = new Logger("pay-peer-push-credit.ts");
export async function preparePeerPushCredit(
ws: InternalWalletState,
req: PreparePeerPushCreditRequest,
): 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.peerPushCredit])
.runReadOnly(async (tx) => {
const existingPushInc =
await tx.peerPushCredit.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,
peerPushCreditId: existing.existingPushInc.peerPushCreditId,
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId: existing.existingPushInc.peerPushCreditId,
}),
};
}
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.fetch(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.fetch(getPurseUrl.href);
const purseStatus = await readSuccessResponseJsonOrThrow(
purseHttpResp,
codecForExchangePurseStatus(),
);
const peerPushCreditId = 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.peerPushCredit])
.runReadWrite(async (tx) => {
await tx.peerPushCredit.add({
peerPushCreditId,
contractPriv: contractPriv,
exchangeBaseUrl: exchangeBaseUrl,
mergePriv: dec.mergePriv,
pursePub: pursePub,
timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
contractTermsHash,
status: PeerPushCreditStatus.DialogProposed,
withdrawalGroupId,
currency: Amounts.currencyOf(purseStatus.balance),
estimatedAmountEffective: Amounts.stringify(
wi.withdrawalAmountEffective,
),
});
await tx.contractTerms.put({
h: contractTermsHash,
contractTermsRaw: dec.contractTerms,
});
});
ws.notify({ type: NotificationType.BalanceChange });
return {
amount: purseStatus.balance,
amountEffective: wi.withdrawalAmountEffective,
amountRaw: purseStatus.balance,
contractTerms: dec.contractTerms,
peerPushCreditId,
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId,
}),
};
}
async function longpollKycStatus(
ws: InternalWalletState,
peerPushCreditId: string,
exchangeUrl: string,
kycInfo: KycPendingInfo,
userType: KycUserType,
): Promise<TaskRunResult> {
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId,
});
const retryTag = constructTaskIdentifier({
tag: PendingTaskType.PeerPushCredit,
peerPushCreditId,
});
runLongpollAsync(ws, retryTag, async (ct) => {
const url = new URL(
`kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
exchangeUrl,
);
url.searchParams.set("timeout_ms", "10000");
logger.info(`kyc url ${url.href}`);
const kycStatusRes = await ws.http.fetch(url.href, {
method: "GET",
cancellationToken: ct,
});
if (
kycStatusRes.status === HttpStatusCode.Ok ||
//FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
// remove after the exchange is fixed or clarified
kycStatusRes.status === HttpStatusCode.NoContent
) {
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
return;
}
if (peerInc.status !== PeerPushCreditStatus.PendingMergeKycRequired) {
return;
}
const oldTxState = computePeerPushCreditTransactionState(peerInc);
peerInc.status = PeerPushCreditStatus.PendingMerge;
const newTxState = computePeerPushCreditTransactionState(peerInc);
await tx.peerPushCredit.put(peerInc);
return { oldTxState, newTxState };
});
notifyTransition(ws, transactionId, transitionInfo);
return { ready: true };
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
// FIXME: Do we have to update the URL here?
return { ready: false };
} else {
throw Error(
`unexpected response from kyc-check (${kycStatusRes.status})`,
);
}
});
return {
type: TaskRunResultType.Longpoll,
};
}
async function processPeerPushCreditKycRequired(
ws: InternalWalletState,
peerInc: PeerPushPaymentIncomingRecord,
kycPending: WalletKycUuid,
): Promise<TaskRunResult> {
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId: peerInc.peerPushCreditId,
});
const { peerPushCreditId } = peerInc;
const userType = "individual";
const url = new URL(
`kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`,
peerInc.exchangeBaseUrl,
);
logger.info(`kyc url ${url.href}`);
const kycStatusRes = await ws.http.fetch(url.href, {
method: "GET",
});
if (
kycStatusRes.status === HttpStatusCode.Ok ||
//FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
// remove after the exchange is fixed or clarified
kycStatusRes.status === HttpStatusCode.NoContent
) {
logger.warn("kyc requested, but already fulfilled");
return TaskRunResult.finished();
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
const kycStatus = await kycStatusRes.json();
logger.info(`kyc status: ${j2s(kycStatus)}`);
const { transitionInfo, result } = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
return {
transitionInfo: undefined,
result: TaskRunResult.finished(),
};
}
const oldTxState = computePeerPushCreditTransactionState(peerInc);
peerInc.kycInfo = {
paytoHash: kycPending.h_payto,
requirementRow: kycPending.requirement_row,
};
peerInc.kycUrl = kycStatus.kyc_url;
peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired;
const newTxState = computePeerPushCreditTransactionState(peerInc);
await tx.peerPushCredit.put(peerInc);
// We'll remove this eventually! New clients should rely on the
// kycUrl field of the transaction, not the error code.
const res: TaskRunResult = {
type: TaskRunResultType.Error,
errorDetail: makeErrorDetail(
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
{
kycUrl: kycStatus.kyc_url,
},
),
};
return {
transitionInfo: { oldTxState, newTxState },
result: res,
};
});
notifyTransition(ws, transactionId, transitionInfo);
return result;
} else {
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
}
}
async function handlePendingMerge(
ws: InternalWalletState,
peerInc: PeerPushPaymentIncomingRecord,
contractTerms: PeerContractTerms,
): Promise<TaskRunResult> {
const { peerPushCreditId } = peerInc;
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId,
});
const amount = Amounts.parseOrThrow(contractTerms.amount);
const mergeReserveInfo = await getMergeReserveInfo(ws, {
exchangeBaseUrl: peerInc.exchangeBaseUrl,
});
const mergeTimestamp = TalerProtocolTimestamp.now();
const reservePayto = talerPaytoFromExchangeReserve(
peerInc.exchangeBaseUrl,
mergeReserveInfo.reservePub,
);
const sigRes = await ws.cryptoApi.signPurseMerge({
contractTermsHash: ContractTermsUtil.hashContractTerms(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.fetch(mergePurseUrl.href, {
method: "POST",
body: mergeReq,
});
if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
const respJson = await mergeHttpResp.json();
const kycPending = codecForWalletKycUuid().decode(respJson);
logger.info(`kyc uuid response: ${j2s(kycPending)}`);
return processPeerPushCreditKycRequired(ws, peerInc, kycPending);
}
logger.trace(`merge request: ${j2s(mergeReq)}`);
const res = await readSuccessResponseJsonOrThrow(
mergeHttpResp,
codecForAny(),
);
logger.trace(`merge response: ${j2s(res)}`);
const withdrawalGroupPrep = await internalPrepareCreateWithdrawalGroup(ws, {
amount,
wgInfo: {
withdrawalType: WithdrawalRecordType.PeerPushCredit,
},
forcedWithdrawalGroupId: peerInc.withdrawalGroupId,
exchangeBaseUrl: peerInc.exchangeBaseUrl,
reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
reserveKeyPair: {
priv: mergeReserveInfo.reservePriv,
pub: mergeReserveInfo.reservePub,
},
});
const txRes = await ws.db
.mktx((x) => [
x.contractTerms,
x.peerPushCredit,
x.withdrawalGroups,
x.reserves,
x.exchanges,
x.exchangeDetails,
])
.runReadWrite(async (tx) => {
const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
return undefined;
}
let withdrawalTransition: TransitionInfo | undefined;
const oldTxState = computePeerPushCreditTransactionState(peerInc);
switch (peerInc.status) {
case PeerPushCreditStatus.PendingMerge:
case PeerPushCreditStatus.PendingMergeKycRequired: {
peerInc.status = PeerPushCreditStatus.PendingWithdrawing;
const wgRes = await internalPerformCreateWithdrawalGroup(
ws,
tx,
withdrawalGroupPrep,
);
peerInc.withdrawalGroupId = wgRes.withdrawalGroup.withdrawalGroupId;
break;
}
}
await tx.peerPushCredit.put(peerInc);
const newTxState = computePeerPushCreditTransactionState(peerInc);
return {
peerPushCreditTransition: { oldTxState, newTxState },
withdrawalTransition,
};
});
notifyTransition(
ws,
withdrawalGroupPrep.transactionId,
txRes?.withdrawalTransition,
);
notifyTransition(ws, transactionId, txRes?.peerPushCreditTransition);
return TaskRunResult.finished();
}
async function handlePendingWithdrawing(
ws: InternalWalletState,
peerInc: PeerPushPaymentIncomingRecord,
): Promise<TaskRunResult> {
if (!peerInc.withdrawalGroupId) {
throw Error("invalid db state (withdrawing, but no withdrawal group ID");
}
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId: peerInc.peerPushCreditId,
});
const wgId = peerInc.withdrawalGroupId;
let finished: boolean = false;
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit, x.withdrawalGroups])
.runReadWrite(async (tx) => {
const ppi = await tx.peerPushCredit.get(peerInc.peerPushCreditId);
if (!ppi) {
finished = true;
return;
}
if (ppi.status !== PeerPushCreditStatus.PendingWithdrawing) {
finished = true;
return;
}
const oldTxState = computePeerPushCreditTransactionState(ppi);
const wg = await tx.withdrawalGroups.get(wgId);
if (!wg) {
// FIXME: Fail the operation instead?
return undefined;
}
switch (wg.status) {
case WithdrawalGroupStatus.Done:
finished = true;
ppi.status = PeerPushCreditStatus.Done;
break;
// FIXME: Also handle other final states!
}
await tx.peerPushCredit.put(ppi);
const newTxState = computePeerPushCreditTransactionState(ppi);
return {
oldTxState,
newTxState,
};
});
notifyTransition(ws, transactionId, transitionInfo);
if (finished) {
return TaskRunResult.finished();
} else {
// FIXME: Return indicator that we depend on the other operation!
return TaskRunResult.pending();
}
}
export async function processPeerPushCredit(
ws: InternalWalletState,
peerPushCreditId: string,
): Promise<TaskRunResult> {
let peerInc: PeerPushPaymentIncomingRecord | undefined;
let contractTerms: PeerContractTerms | undefined;
await ws.db
.mktx((x) => [x.contractTerms, x.peerPushCredit])
.runReadWrite(async (tx) => {
peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
return;
}
const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash);
if (ctRec) {
contractTerms = ctRec.contractTermsRaw;
}
await tx.peerPushCredit.put(peerInc);
});
checkDbInvariant(!!contractTerms);
if (!peerInc) {
throw Error(
`can't accept unknown incoming p2p push payment (${peerPushCreditId})`,
);
}
switch (peerInc.status) {
case PeerPushCreditStatus.PendingMergeKycRequired: {
if (!peerInc.kycInfo) {
throw Error("invalid state, kycInfo required");
}
return await longpollKycStatus(
ws,
peerPushCreditId,
peerInc.exchangeBaseUrl,
peerInc.kycInfo,
"individual",
);
}
case PeerPushCreditStatus.PendingMerge:
return handlePendingMerge(ws, peerInc, contractTerms);
case PeerPushCreditStatus.PendingWithdrawing:
return handlePendingWithdrawing(ws, peerInc);
default:
return TaskRunResult.finished();
}
}
export async function confirmPeerPushCredit(
ws: InternalWalletState,
req: ConfirmPeerPushCreditRequest,
): Promise<AcceptPeerPushPaymentResponse> {
let peerInc: PeerPushPaymentIncomingRecord | undefined;
let peerPushCreditId: string;
if (req.peerPushCreditId) {
peerPushCreditId = req.peerPushCreditId;
} else if (req.transactionId) {
const parsedTx = parseTransactionIdentifier(req.transactionId);
if (!parsedTx) {
throw Error("invalid transaction ID");
}
if (parsedTx.tag !== TransactionType.PeerPushCredit) {
throw Error("invalid transaction ID type");
}
peerPushCreditId = parsedTx.peerPushCreditId;
} else {
throw Error("no transaction ID (or deprecated peerPushCreditId) provided");
}
await ws.db
.mktx((x) => [x.contractTerms, x.peerPushCredit])
.runReadWrite(async (tx) => {
peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
return;
}
if (peerInc.status === PeerPushCreditStatus.DialogProposed) {
peerInc.status = PeerPushCreditStatus.PendingMerge;
}
await tx.peerPushCredit.put(peerInc);
});
if (!peerInc) {
throw Error(
`can't accept unknown incoming p2p push payment (${req.peerPushCreditId})`,
);
}
ws.workAvailable.trigger();
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId,
});
return {
transactionId,
};
}
export async function suspendPeerPushCreditTransaction(
ws: InternalWalletState,
peerPushCreditId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPushCredit,
peerPushCreditId,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
logger.warn(`peer push credit ${peerPushCreditId} not found`);
return;
}
let newStatus: PeerPushCreditStatus | undefined = undefined;
switch (pushCreditRec.status) {
case PeerPushCreditStatus.DialogProposed:
case PeerPushCreditStatus.Done:
case PeerPushCreditStatus.SuspendedMerge:
case PeerPushCreditStatus.SuspendedMergeKycRequired:
case PeerPushCreditStatus.SuspendedWithdrawing:
break;
case PeerPushCreditStatus.PendingMergeKycRequired:
newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
break;
case PeerPushCreditStatus.PendingMerge:
newStatus = PeerPushCreditStatus.SuspendedMerge;
break;
case PeerPushCreditStatus.PendingWithdrawing:
// FIXME: Suspend internal withdrawal transaction!
newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
break;
case PeerPushCreditStatus.Aborted:
break;
case PeerPushCreditStatus.Failed:
break;
default:
assertUnreachable(pushCreditRec.status);
}
if (newStatus != null) {
const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
pushCreditRec.status = newStatus;
const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
await tx.peerPushCredit.put(pushCreditRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
}
export async function abortPeerPushCreditTransaction(
ws: InternalWalletState,
peerPushCreditId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPushCredit,
peerPushCreditId,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
logger.warn(`peer push credit ${peerPushCreditId} not found`);
return;
}
let newStatus: PeerPushCreditStatus | undefined = undefined;
switch (pushCreditRec.status) {
case PeerPushCreditStatus.DialogProposed:
newStatus = PeerPushCreditStatus.Aborted;
break;
case PeerPushCreditStatus.Done:
break;
case PeerPushCreditStatus.SuspendedMerge:
case PeerPushCreditStatus.SuspendedMergeKycRequired:
case PeerPushCreditStatus.SuspendedWithdrawing:
newStatus = PeerPushCreditStatus.Aborted;
break;
case PeerPushCreditStatus.PendingMergeKycRequired:
newStatus = PeerPushCreditStatus.Aborted;
break;
case PeerPushCreditStatus.PendingMerge:
newStatus = PeerPushCreditStatus.Aborted;
break;
case PeerPushCreditStatus.PendingWithdrawing:
newStatus = PeerPushCreditStatus.Aborted;
break;
case PeerPushCreditStatus.Aborted:
break;
case PeerPushCreditStatus.Failed:
break;
default:
assertUnreachable(pushCreditRec.status);
}
if (newStatus != null) {
const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
pushCreditRec.status = newStatus;
const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
await tx.peerPushCredit.put(pushCreditRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
}
export async function failPeerPushCreditTransaction(
ws: InternalWalletState,
peerPushCreditId: 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,
peerPushCreditId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPushCredit,
peerPushCreditId,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
logger.warn(`peer push credit ${peerPushCreditId} not found`);
return;
}
let newStatus: PeerPushCreditStatus | undefined = undefined;
switch (pushCreditRec.status) {
case PeerPushCreditStatus.DialogProposed:
case PeerPushCreditStatus.Done:
case PeerPushCreditStatus.PendingMergeKycRequired:
case PeerPushCreditStatus.PendingMerge:
case PeerPushCreditStatus.PendingWithdrawing:
case PeerPushCreditStatus.SuspendedMerge:
newStatus = PeerPushCreditStatus.PendingMerge;
break;
case PeerPushCreditStatus.SuspendedMergeKycRequired:
newStatus = PeerPushCreditStatus.PendingMergeKycRequired;
break;
case PeerPushCreditStatus.SuspendedWithdrawing:
// FIXME: resume underlying "internal-withdrawal" transaction.
newStatus = PeerPushCreditStatus.PendingWithdrawing;
break;
case PeerPushCreditStatus.Aborted:
break;
case PeerPushCreditStatus.Failed:
break;
default:
assertUnreachable(pushCreditRec.status);
}
if (newStatus != null) {
const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
pushCreditRec.status = newStatus;
const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
await tx.peerPushCredit.put(pushCreditRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
ws.workAvailable.trigger();
notifyTransition(ws, transactionId, transitionInfo);
}
export function computePeerPushCreditTransactionState(
pushCreditRecord: PeerPushPaymentIncomingRecord,
): TransactionState {
switch (pushCreditRecord.status) {
case PeerPushCreditStatus.DialogProposed:
return {
major: TransactionMajorState.Dialog,
minor: TransactionMinorState.Proposed,
};
case PeerPushCreditStatus.PendingMerge:
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Merge,
};
case PeerPushCreditStatus.Done:
return {
major: TransactionMajorState.Done,
};
case PeerPushCreditStatus.PendingMergeKycRequired:
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.KycRequired,
};
case PeerPushCreditStatus.PendingWithdrawing:
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Withdraw,
};
case PeerPushCreditStatus.SuspendedMerge:
return {
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.Merge,
};
case PeerPushCreditStatus.SuspendedMergeKycRequired:
return {
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.MergeKycRequired,
};
case PeerPushCreditStatus.SuspendedWithdrawing:
return {
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.Withdraw,
};
case PeerPushCreditStatus.Aborted:
return {
major: TransactionMajorState.Aborted,
};
case PeerPushCreditStatus.Failed:
return {
major: TransactionMajorState.Failed,
};
default:
assertUnreachable(pushCreditRecord.status);
}
}
export function computePeerPushCreditTransactionActions(
pushCreditRecord: PeerPushPaymentIncomingRecord,
): TransactionAction[] {
switch (pushCreditRecord.status) {
case PeerPushCreditStatus.DialogProposed:
return [TransactionAction.Delete];
case PeerPushCreditStatus.PendingMerge:
return [TransactionAction.Abort, TransactionAction.Suspend];
case PeerPushCreditStatus.Done:
return [TransactionAction.Delete];
case PeerPushCreditStatus.PendingMergeKycRequired:
return [TransactionAction.Abort, TransactionAction.Suspend];
case PeerPushCreditStatus.PendingWithdrawing:
return [TransactionAction.Suspend, TransactionAction.Fail];
case PeerPushCreditStatus.SuspendedMerge:
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPushCreditStatus.SuspendedMergeKycRequired:
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPushCreditStatus.SuspendedWithdrawing:
return [TransactionAction.Resume, TransactionAction.Fail];
case PeerPushCreditStatus.Aborted:
return [TransactionAction.Delete];
case PeerPushCreditStatus.Failed:
return [TransactionAction.Delete];
default:
assertUnreachable(pushCreditRecord.status);
}
}