770 lines
24 KiB
TypeScript
770 lines
24 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 {
|
||
|
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);
|
||
|
}
|
||
|
}
|