summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts770
1 files changed, 770 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
new file mode 100644
index 000000000..69e0f3c27
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -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);
+ }
+} \ No newline at end of file