934 lines
29 KiB
TypeScript
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);
|
|
}
|
|
}
|