1183 lines
39 KiB
TypeScript
1183 lines
39 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 {
|
|
AbsoluteTime,
|
|
Amounts,
|
|
CancellationToken,
|
|
CheckPeerPullCreditRequest,
|
|
CheckPeerPullCreditResponse,
|
|
ContractTermsUtil,
|
|
ExchangeReservePurseRequest,
|
|
HttpStatusCode,
|
|
InitiatePeerPullCreditRequest,
|
|
InitiatePeerPullCreditResponse,
|
|
Logger,
|
|
TalerErrorCode,
|
|
TalerPreciseTimestamp,
|
|
TalerUriAction,
|
|
TransactionAction,
|
|
TransactionMajorState,
|
|
TransactionMinorState,
|
|
TransactionState,
|
|
TransactionType,
|
|
WalletAccountMergeFlags,
|
|
WalletKycUuid,
|
|
codecForAny,
|
|
codecForWalletKycUuid,
|
|
encodeCrock,
|
|
getRandomBytes,
|
|
j2s,
|
|
makeErrorDetail,
|
|
stringifyTalerUri,
|
|
} from "@gnu-taler/taler-util";
|
|
import {
|
|
readSuccessResponseJsonOrErrorCode,
|
|
readSuccessResponseJsonOrThrow,
|
|
throwUnexpectedRequestError,
|
|
} from "@gnu-taler/taler-util/http";
|
|
import {
|
|
KycPendingInfo,
|
|
KycUserType,
|
|
PeerPullPaymentInitiationRecord,
|
|
PeerPullPaymentInitiationStatus,
|
|
WithdrawalGroupStatus,
|
|
WithdrawalRecordType,
|
|
updateExchangeFromUrl,
|
|
} from "../index.js";
|
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
|
import { PendingTaskType } from "../pending-types.js";
|
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
|
import { checkDbInvariant } from "../util/invariants.js";
|
|
import {
|
|
OperationAttemptResult,
|
|
OperationAttemptResultType,
|
|
constructTaskIdentifier,
|
|
} from "../util/retries.js";
|
|
import {
|
|
LongpollResult,
|
|
resetOperationTimeout,
|
|
runLongpollAsync,
|
|
runOperationWithErrorReporting,
|
|
} from "./common.js";
|
|
import {
|
|
codecForExchangePurseStatus,
|
|
getMergeReserveInfo,
|
|
talerPaytoFromExchangeReserve,
|
|
} from "./pay-peer-common.js";
|
|
import {
|
|
constructTransactionIdentifier,
|
|
notifyTransition,
|
|
stopLongpolling,
|
|
} from "./transactions.js";
|
|
import {
|
|
getExchangeWithdrawalInfo,
|
|
internalCreateWithdrawalGroup,
|
|
processWithdrawalGroup,
|
|
} from "./withdraw.js";
|
|
|
|
const logger = new Logger("pay-peer-pull-credit.ts");
|
|
|
|
async function queryPurseForPeerPullCredit(
|
|
ws: InternalWalletState,
|
|
pullIni: PeerPullPaymentInitiationRecord,
|
|
cancellationToken: CancellationToken,
|
|
): Promise<LongpollResult> {
|
|
const purseDepositUrl = new URL(
|
|
`purses/${pullIni.pursePub}/deposit`,
|
|
pullIni.exchangeBaseUrl,
|
|
);
|
|
purseDepositUrl.searchParams.set("timeout_ms", "30000");
|
|
logger.info(`querying purse status via ${purseDepositUrl.href}`);
|
|
const resp = await ws.http.fetch(purseDepositUrl.href, {
|
|
timeout: { d_ms: 60000 },
|
|
cancellationToken,
|
|
});
|
|
|
|
logger.info(`purse status code: HTTP ${resp.status}`);
|
|
|
|
const result = await readSuccessResponseJsonOrErrorCode(
|
|
resp,
|
|
codecForExchangePurseStatus(),
|
|
);
|
|
|
|
if (result.isError) {
|
|
logger.info(`got purse status error, EC=${result.talerErrorResponse.code}`);
|
|
if (resp.status === 404) {
|
|
return { ready: false };
|
|
} else {
|
|
throwUnexpectedRequestError(resp, result.talerErrorResponse);
|
|
}
|
|
}
|
|
|
|
if (!result.response.deposit_timestamp) {
|
|
logger.info("purse not ready yet (no deposit)");
|
|
return { ready: false };
|
|
}
|
|
|
|
const reserve = await ws.db
|
|
.mktx((x) => [x.reserves])
|
|
.runReadOnly(async (tx) => {
|
|
return await tx.reserves.get(pullIni.mergeReserveRowId);
|
|
});
|
|
|
|
if (!reserve) {
|
|
throw Error("reserve for peer pull credit not found in wallet DB");
|
|
}
|
|
|
|
await internalCreateWithdrawalGroup(ws, {
|
|
amount: Amounts.parseOrThrow(pullIni.amount),
|
|
wgInfo: {
|
|
withdrawalType: WithdrawalRecordType.PeerPullCredit,
|
|
contractTerms: pullIni.contractTerms,
|
|
contractPriv: pullIni.contractPriv,
|
|
},
|
|
forcedWithdrawalGroupId: pullIni.withdrawalGroupId,
|
|
exchangeBaseUrl: pullIni.exchangeBaseUrl,
|
|
reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
|
|
reserveKeyPair: {
|
|
priv: reserve.reservePriv,
|
|
pub: reserve.reservePub,
|
|
},
|
|
});
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPullCredit,
|
|
pursePub: pullIni.pursePub,
|
|
});
|
|
const transitionInfo = await ws.db
|
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
|
.runReadWrite(async (tx) => {
|
|
const finPi = await tx.peerPullPaymentInitiations.get(pullIni.pursePub);
|
|
if (!finPi) {
|
|
logger.warn("peerPullPaymentInitiation not found anymore");
|
|
return;
|
|
}
|
|
const oldTxState = computePeerPullCreditTransactionState(finPi);
|
|
if (finPi.status === PeerPullPaymentInitiationStatus.PendingReady) {
|
|
finPi.status = PeerPullPaymentInitiationStatus.PendingWithdrawing;
|
|
}
|
|
await tx.peerPullPaymentInitiations.put(finPi);
|
|
const newTxState = computePeerPullCreditTransactionState(finPi);
|
|
return { oldTxState, newTxState };
|
|
});
|
|
notifyTransition(ws, transactionId, transitionInfo);
|
|
return {
|
|
ready: true,
|
|
};
|
|
}
|
|
|
|
async function longpollKycStatus(
|
|
ws: InternalWalletState,
|
|
pursePub: string,
|
|
exchangeUrl: string,
|
|
kycInfo: KycPendingInfo,
|
|
userType: KycUserType,
|
|
): Promise<OperationAttemptResult> {
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
const retryTag = constructTaskIdentifier({
|
|
tag: PendingTaskType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
|
|
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.peerPullPaymentInitiations])
|
|
.runReadWrite(async (tx) => {
|
|
const peerIni = await tx.peerPullPaymentInitiations.get(pursePub);
|
|
if (!peerIni) {
|
|
return;
|
|
}
|
|
if (
|
|
peerIni.status !==
|
|
PeerPullPaymentInitiationStatus.PendingMergeKycRequired
|
|
) {
|
|
return;
|
|
}
|
|
const oldTxState = computePeerPullCreditTransactionState(peerIni);
|
|
peerIni.status = PeerPullPaymentInitiationStatus.PendingCreatePurse;
|
|
const newTxState = computePeerPullCreditTransactionState(peerIni);
|
|
await tx.peerPullPaymentInitiations.put(peerIni);
|
|
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: OperationAttemptResultType.Longpoll,
|
|
};
|
|
}
|
|
|
|
async function processPeerPullCreditAbortingDeletePurse(
|
|
ws: InternalWalletState,
|
|
peerPullIni: PeerPullPaymentInitiationRecord,
|
|
): Promise<OperationAttemptResult> {
|
|
const { pursePub, pursePriv } = peerPullIni;
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPushDebit,
|
|
pursePub,
|
|
});
|
|
|
|
const sigResp = await ws.cryptoApi.signDeletePurse({
|
|
pursePriv,
|
|
});
|
|
const purseUrl = new URL(`purses/${pursePub}`, peerPullIni.exchangeBaseUrl);
|
|
const resp = await ws.http.fetch(purseUrl.href, {
|
|
method: "DELETE",
|
|
headers: {
|
|
"taler-purse-signature": sigResp.sig,
|
|
},
|
|
});
|
|
logger.info(`deleted purse with response status ${resp.status}`);
|
|
|
|
const transitionInfo = await ws.db
|
|
.mktx((x) => [
|
|
x.peerPullPaymentInitiations,
|
|
x.refreshGroups,
|
|
x.denominations,
|
|
x.coinAvailability,
|
|
x.coins,
|
|
])
|
|
.runReadWrite(async (tx) => {
|
|
const ppiRec = await tx.peerPullPaymentInitiations.get(pursePub);
|
|
if (!ppiRec) {
|
|
return undefined;
|
|
}
|
|
if (
|
|
ppiRec.status !== PeerPullPaymentInitiationStatus.AbortingDeletePurse
|
|
) {
|
|
return undefined;
|
|
}
|
|
const oldTxState = computePeerPullCreditTransactionState(ppiRec);
|
|
ppiRec.status = PeerPullPaymentInitiationStatus.Aborted;
|
|
const newTxState = computePeerPullCreditTransactionState(ppiRec);
|
|
return {
|
|
oldTxState,
|
|
newTxState,
|
|
};
|
|
});
|
|
notifyTransition(ws, transactionId, transitionInfo);
|
|
|
|
return OperationAttemptResult.pendingEmpty();
|
|
}
|
|
|
|
async function handlePeerPullCreditWithdrawing(
|
|
ws: InternalWalletState,
|
|
pullIni: PeerPullPaymentInitiationRecord,
|
|
): Promise<OperationAttemptResult> {
|
|
if (!pullIni.withdrawalGroupId) {
|
|
throw Error("invalid db state (withdrawing, but no withdrawal group ID");
|
|
}
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPullCredit,
|
|
pursePub: pullIni.pursePub,
|
|
});
|
|
const wgId = pullIni.withdrawalGroupId;
|
|
let finished: boolean = false;
|
|
const transitionInfo = await ws.db
|
|
.mktx((x) => [x.peerPullPaymentInitiations, x.withdrawalGroups])
|
|
.runReadWrite(async (tx) => {
|
|
const ppi = await tx.peerPullPaymentInitiations.get(
|
|
pullIni.pursePub,
|
|
);
|
|
if (!ppi) {
|
|
finished = true;
|
|
return;
|
|
}
|
|
if (ppi.status !== PeerPullPaymentInitiationStatus.PendingWithdrawing) {
|
|
finished = true;
|
|
return;
|
|
}
|
|
const oldTxState = computePeerPullCreditTransactionState(ppi);
|
|
const wg = await tx.withdrawalGroups.get(wgId);
|
|
if (!wg) {
|
|
// FIXME: Fail the operation instead?
|
|
return undefined;
|
|
}
|
|
switch (wg.status) {
|
|
case WithdrawalGroupStatus.Finished:
|
|
finished = true;
|
|
ppi.status = PeerPullPaymentInitiationStatus.Done;
|
|
break;
|
|
// FIXME: Also handle other final states!
|
|
}
|
|
await tx.peerPullPaymentInitiations.put(ppi);
|
|
const newTxState = computePeerPullCreditTransactionState(ppi);
|
|
return {
|
|
oldTxState,
|
|
newTxState,
|
|
};
|
|
});
|
|
notifyTransition(ws, transactionId, transitionInfo);
|
|
if (finished) {
|
|
return OperationAttemptResult.finishedEmpty();
|
|
} else {
|
|
// FIXME: Return indicator that we depend on the other operation!
|
|
return OperationAttemptResult.pendingEmpty();
|
|
}
|
|
}
|
|
|
|
async function handlePeerPullCreditCreatePurse(
|
|
ws: InternalWalletState,
|
|
pullIni: PeerPullPaymentInitiationRecord,
|
|
): Promise<OperationAttemptResult> {
|
|
const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount));
|
|
const pursePub = pullIni.pursePub;
|
|
const mergeReserve = await ws.db
|
|
.mktx((x) => [x.reserves])
|
|
.runReadOnly(async (tx) => {
|
|
return tx.reserves.get(pullIni.mergeReserveRowId);
|
|
});
|
|
|
|
if (!mergeReserve) {
|
|
throw Error("merge reserve for peer pull payment not found in database");
|
|
}
|
|
|
|
const reservePayto = talerPaytoFromExchangeReserve(
|
|
pullIni.exchangeBaseUrl,
|
|
mergeReserve.reservePub,
|
|
);
|
|
|
|
const econtractResp = await ws.cryptoApi.encryptContractForDeposit({
|
|
contractPriv: pullIni.contractPriv,
|
|
contractPub: pullIni.contractPub,
|
|
contractTerms: pullIni.contractTerms,
|
|
pursePriv: pullIni.pursePriv,
|
|
pursePub: pullIni.pursePub,
|
|
nonce: pullIni.contractEncNonce,
|
|
});
|
|
|
|
const purseExpiration = pullIni.contractTerms.purse_expiration;
|
|
const sigRes = await ws.cryptoApi.signReservePurseCreate({
|
|
contractTermsHash: pullIni.contractTermsHash,
|
|
flags: WalletAccountMergeFlags.CreateWithPurseFee,
|
|
mergePriv: pullIni.mergePriv,
|
|
mergeTimestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp),
|
|
purseAmount: pullIni.contractTerms.amount,
|
|
purseExpiration: purseExpiration,
|
|
purseFee: purseFee,
|
|
pursePriv: pullIni.pursePriv,
|
|
pursePub: pullIni.pursePub,
|
|
reservePayto,
|
|
reservePriv: mergeReserve.reservePriv,
|
|
});
|
|
|
|
const reservePurseReqBody: ExchangeReservePurseRequest = {
|
|
merge_sig: sigRes.mergeSig,
|
|
merge_timestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp),
|
|
h_contract_terms: pullIni.contractTermsHash,
|
|
merge_pub: pullIni.mergePub,
|
|
min_age: 0,
|
|
purse_expiration: purseExpiration,
|
|
purse_fee: purseFee,
|
|
purse_pub: pullIni.pursePub,
|
|
purse_sig: sigRes.purseSig,
|
|
purse_value: pullIni.contractTerms.amount,
|
|
reserve_sig: sigRes.accountSig,
|
|
econtract: econtractResp.econtract,
|
|
};
|
|
|
|
logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
|
|
|
|
const reservePurseMergeUrl = new URL(
|
|
`reserves/${mergeReserve.reservePub}/purse`,
|
|
pullIni.exchangeBaseUrl,
|
|
);
|
|
|
|
const httpResp = await ws.http.postJson(
|
|
reservePurseMergeUrl.href,
|
|
reservePurseReqBody,
|
|
);
|
|
|
|
if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
|
|
const respJson = await httpResp.json();
|
|
const kycPending = codecForWalletKycUuid().decode(respJson);
|
|
logger.info(`kyc uuid response: ${j2s(kycPending)}`);
|
|
return processPeerPullCreditKycRequired(ws, pullIni, kycPending);
|
|
}
|
|
|
|
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
|
|
|
logger.info(`reserve merge response: ${j2s(resp)}`);
|
|
|
|
await ws.db
|
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
|
.runReadWrite(async (tx) => {
|
|
const pi2 = await tx.peerPullPaymentInitiations.get(pursePub);
|
|
if (!pi2) {
|
|
return;
|
|
}
|
|
pi2.status = PeerPullPaymentInitiationStatus.PendingReady;
|
|
await tx.peerPullPaymentInitiations.put(pi2);
|
|
});
|
|
|
|
return {
|
|
type: OperationAttemptResultType.Finished,
|
|
result: undefined,
|
|
};
|
|
}
|
|
|
|
export async function processPeerPullCredit(
|
|
ws: InternalWalletState,
|
|
pursePub: string,
|
|
): Promise<OperationAttemptResult> {
|
|
const pullIni = await ws.db
|
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
|
.runReadOnly(async (tx) => {
|
|
return tx.peerPullPaymentInitiations.get(pursePub);
|
|
});
|
|
if (!pullIni) {
|
|
throw Error("peer pull payment initiation not found in database");
|
|
}
|
|
|
|
const retryTag = constructTaskIdentifier({
|
|
tag: PendingTaskType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
|
|
// We're already running!
|
|
if (ws.activeLongpoll[retryTag]) {
|
|
logger.info("peer-pull-credit already in long-polling, returning!");
|
|
return {
|
|
type: OperationAttemptResultType.Longpoll,
|
|
};
|
|
}
|
|
|
|
logger.trace(`processing ${retryTag}, status=${pullIni.status}`);
|
|
|
|
switch (pullIni.status) {
|
|
case PeerPullPaymentInitiationStatus.Done: {
|
|
// We implement this case so that the "retry" action on a peer-pull-credit transaction
|
|
// also retries the withdrawal task.
|
|
|
|
logger.warn(
|
|
"peer pull payment initiation is already finished, retrying withdrawal",
|
|
);
|
|
|
|
const withdrawalGroupId = pullIni.withdrawalGroupId;
|
|
|
|
if (withdrawalGroupId) {
|
|
const taskId = constructTaskIdentifier({
|
|
tag: PendingTaskType.Withdraw,
|
|
withdrawalGroupId,
|
|
});
|
|
stopLongpolling(ws, taskId);
|
|
await resetOperationTimeout(ws, taskId);
|
|
await runOperationWithErrorReporting(ws, taskId, () =>
|
|
processWithdrawalGroup(ws, withdrawalGroupId),
|
|
);
|
|
}
|
|
return {
|
|
type: OperationAttemptResultType.Finished,
|
|
result: undefined,
|
|
};
|
|
}
|
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
|
runLongpollAsync(ws, retryTag, async (cancellationToken) =>
|
|
queryPurseForPeerPullCredit(ws, pullIni, cancellationToken),
|
|
);
|
|
logger.trace(
|
|
"returning early from processPeerPullCredit for long-polling in background",
|
|
);
|
|
return {
|
|
type: OperationAttemptResultType.Longpoll,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: {
|
|
if (!pullIni.kycInfo) {
|
|
throw Error("invalid state, kycInfo required");
|
|
}
|
|
return await longpollKycStatus(
|
|
ws,
|
|
pursePub,
|
|
pullIni.exchangeBaseUrl,
|
|
pullIni.kycInfo,
|
|
"individual",
|
|
);
|
|
}
|
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
|
return handlePeerPullCreditCreatePurse(ws, pullIni);
|
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
|
return await processPeerPullCreditAbortingDeletePurse(ws, pullIni);
|
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
|
return handlePeerPullCreditWithdrawing(ws, pullIni);
|
|
case PeerPullPaymentInitiationStatus.Aborted:
|
|
case PeerPullPaymentInitiationStatus.Failed:
|
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
|
break;
|
|
default:
|
|
assertUnreachable(pullIni.status);
|
|
}
|
|
|
|
return OperationAttemptResult.finishedEmpty();
|
|
}
|
|
|
|
async function processPeerPullCreditKycRequired(
|
|
ws: InternalWalletState,
|
|
peerIni: PeerPullPaymentInitiationRecord,
|
|
kycPending: WalletKycUuid,
|
|
): Promise<OperationAttemptResult> {
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPullCredit,
|
|
pursePub: peerIni.pursePub,
|
|
});
|
|
const { pursePub } = peerIni;
|
|
|
|
const userType = "individual";
|
|
const url = new URL(
|
|
`kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`,
|
|
peerIni.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 {
|
|
type: OperationAttemptResultType.Finished,
|
|
result: undefined,
|
|
};
|
|
} 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.peerPullPaymentInitiations])
|
|
.runReadWrite(async (tx) => {
|
|
const peerInc = await tx.peerPullPaymentInitiations.get(pursePub);
|
|
if (!peerInc) {
|
|
return {
|
|
transitionInfo: undefined,
|
|
result: OperationAttemptResult.finishedEmpty(),
|
|
};
|
|
}
|
|
const oldTxState = computePeerPullCreditTransactionState(peerInc);
|
|
peerInc.kycInfo = {
|
|
paytoHash: kycPending.h_payto,
|
|
requirementRow: kycPending.requirement_row,
|
|
};
|
|
peerInc.kycUrl = kycStatus.kyc_url;
|
|
peerInc.status =
|
|
PeerPullPaymentInitiationStatus.PendingMergeKycRequired;
|
|
const newTxState = computePeerPullCreditTransactionState(peerInc);
|
|
await tx.peerPullPaymentInitiations.put(peerInc);
|
|
// We'll remove this eventually! New clients should rely on the
|
|
// kycUrl field of the transaction, not the error code.
|
|
const res: OperationAttemptResult = {
|
|
type: OperationAttemptResultType.Error,
|
|
errorDetail: makeErrorDetail(
|
|
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
|
|
{
|
|
kycUrl: kycStatus.kyc_url,
|
|
},
|
|
),
|
|
};
|
|
return {
|
|
transitionInfo: { oldTxState, newTxState },
|
|
result: res,
|
|
};
|
|
});
|
|
notifyTransition(ws, transactionId, transitionInfo);
|
|
return {
|
|
type: OperationAttemptResultType.Pending,
|
|
result: undefined,
|
|
};
|
|
} else {
|
|
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check fees and available exchanges for a peer push payment initiation.
|
|
*/
|
|
export async function checkPeerPullPaymentInitiation(
|
|
ws: InternalWalletState,
|
|
req: CheckPeerPullCreditRequest,
|
|
): Promise<CheckPeerPullCreditResponse> {
|
|
// FIXME: We don't support exchanges with purse fees yet.
|
|
// Select an exchange where we have money in the specified currency
|
|
// FIXME: How do we handle regional currency scopes here? Is it an additional input?
|
|
|
|
logger.trace("checking peer-pull-credit fees");
|
|
|
|
const currency = Amounts.currencyOf(req.amount);
|
|
let exchangeUrl;
|
|
if (req.exchangeBaseUrl) {
|
|
exchangeUrl = req.exchangeBaseUrl;
|
|
} else {
|
|
exchangeUrl = await getPreferredExchangeForCurrency(ws, currency);
|
|
}
|
|
|
|
if (!exchangeUrl) {
|
|
throw Error("no exchange found for initiating a peer pull payment");
|
|
}
|
|
|
|
logger.trace(`found ${exchangeUrl} as preferred exchange`);
|
|
|
|
const wi = await getExchangeWithdrawalInfo(
|
|
ws,
|
|
exchangeUrl,
|
|
Amounts.parseOrThrow(req.amount),
|
|
undefined,
|
|
);
|
|
|
|
logger.trace(`got withdrawal info`);
|
|
|
|
return {
|
|
exchangeBaseUrl: exchangeUrl,
|
|
amountEffective: wi.withdrawalAmountEffective,
|
|
amountRaw: req.amount,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Find a preferred exchange based on when we withdrew last from this exchange.
|
|
*/
|
|
async function getPreferredExchangeForCurrency(
|
|
ws: InternalWalletState,
|
|
currency: string,
|
|
): Promise<string | undefined> {
|
|
// Find an exchange with the matching currency.
|
|
// Prefer exchanges with the most recent withdrawal.
|
|
const url = await ws.db
|
|
.mktx((x) => [x.exchanges])
|
|
.runReadOnly(async (tx) => {
|
|
const exchanges = await tx.exchanges.iter().toArray();
|
|
let candidate = undefined;
|
|
for (const e of exchanges) {
|
|
if (e.detailsPointer?.currency !== currency) {
|
|
continue;
|
|
}
|
|
if (!candidate) {
|
|
candidate = e;
|
|
continue;
|
|
}
|
|
if (candidate.lastWithdrawal && !e.lastWithdrawal) {
|
|
continue;
|
|
}
|
|
if (candidate.lastWithdrawal && e.lastWithdrawal) {
|
|
if (
|
|
AbsoluteTime.cmp(
|
|
AbsoluteTime.fromPreciseTimestamp(e.lastWithdrawal),
|
|
AbsoluteTime.fromPreciseTimestamp(candidate.lastWithdrawal),
|
|
) > 0
|
|
) {
|
|
candidate = e;
|
|
}
|
|
}
|
|
}
|
|
if (candidate) {
|
|
return candidate.baseUrl;
|
|
}
|
|
return undefined;
|
|
});
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Initiate a peer pull payment.
|
|
*/
|
|
export async function initiatePeerPullPayment(
|
|
ws: InternalWalletState,
|
|
req: InitiatePeerPullCreditRequest,
|
|
): Promise<InitiatePeerPullCreditResponse> {
|
|
const currency = Amounts.currencyOf(req.partialContractTerms.amount);
|
|
let maybeExchangeBaseUrl: string | undefined;
|
|
if (req.exchangeBaseUrl) {
|
|
maybeExchangeBaseUrl = req.exchangeBaseUrl;
|
|
} else {
|
|
maybeExchangeBaseUrl = await getPreferredExchangeForCurrency(ws, currency);
|
|
}
|
|
|
|
if (!maybeExchangeBaseUrl) {
|
|
throw Error("no exchange found for initiating a peer pull payment");
|
|
}
|
|
|
|
const exchangeBaseUrl = maybeExchangeBaseUrl;
|
|
|
|
await updateExchangeFromUrl(ws, exchangeBaseUrl);
|
|
|
|
const mergeReserveInfo = await getMergeReserveInfo(ws, {
|
|
exchangeBaseUrl: exchangeBaseUrl,
|
|
});
|
|
|
|
const mergeTimestamp = TalerPreciseTimestamp.now();
|
|
|
|
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
|
|
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
|
|
|
|
const contractTerms = req.partialContractTerms;
|
|
|
|
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
|
|
|
|
const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({});
|
|
|
|
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
|
|
|
|
const mergeReserveRowId = mergeReserveInfo.rowId;
|
|
checkDbInvariant(!!mergeReserveRowId);
|
|
|
|
const contractEncNonce = encodeCrock(getRandomBytes(24));
|
|
|
|
const wi = await getExchangeWithdrawalInfo(
|
|
ws,
|
|
exchangeBaseUrl,
|
|
Amounts.parseOrThrow(req.partialContractTerms.amount),
|
|
undefined,
|
|
);
|
|
|
|
await ws.db
|
|
.mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms])
|
|
.runReadWrite(async (tx) => {
|
|
await tx.peerPullPaymentInitiations.put({
|
|
amount: req.partialContractTerms.amount,
|
|
contractTermsHash: hContractTerms,
|
|
exchangeBaseUrl: exchangeBaseUrl,
|
|
pursePriv: pursePair.priv,
|
|
pursePub: pursePair.pub,
|
|
mergePriv: mergePair.priv,
|
|
mergePub: mergePair.pub,
|
|
status: PeerPullPaymentInitiationStatus.PendingCreatePurse,
|
|
contractTerms: contractTerms,
|
|
mergeTimestamp,
|
|
contractEncNonce,
|
|
mergeReserveRowId: mergeReserveRowId,
|
|
contractPriv: contractKeyPair.priv,
|
|
contractPub: contractKeyPair.pub,
|
|
withdrawalGroupId,
|
|
estimatedAmountEffective: wi.withdrawalAmountEffective,
|
|
});
|
|
await tx.contractTerms.put({
|
|
contractTermsRaw: contractTerms,
|
|
h: hContractTerms,
|
|
});
|
|
});
|
|
|
|
// FIXME: Should we somehow signal to the client
|
|
// whether purse creation has failed, or does the client/
|
|
// check this asynchronously from the transaction status?
|
|
|
|
const taskId = constructTaskIdentifier({
|
|
tag: PendingTaskType.PeerPullCredit,
|
|
pursePub: pursePair.pub,
|
|
});
|
|
|
|
await runOperationWithErrorReporting(ws, taskId, async () => {
|
|
return processPeerPullCredit(ws, pursePair.pub);
|
|
});
|
|
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPullCredit,
|
|
pursePub: pursePair.pub,
|
|
});
|
|
|
|
return {
|
|
talerUri: stringifyTalerUri({
|
|
type: TalerUriAction.PayPull,
|
|
exchangeBaseUrl: exchangeBaseUrl,
|
|
contractPriv: contractKeyPair.priv,
|
|
}),
|
|
transactionId,
|
|
};
|
|
}
|
|
|
|
export async function suspendPeerPullCreditTransaction(
|
|
ws: InternalWalletState,
|
|
pursePub: string,
|
|
) {
|
|
const taskId = constructTaskIdentifier({
|
|
tag: PendingTaskType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
stopLongpolling(ws, taskId);
|
|
const transitionInfo = await ws.db
|
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
|
.runReadWrite(async (tx) => {
|
|
const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub);
|
|
if (!pullCreditRec) {
|
|
logger.warn(`peer pull credit ${pursePub} not found`);
|
|
return;
|
|
}
|
|
let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined;
|
|
switch (pullCreditRec.status) {
|
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
|
newStatus = PeerPullPaymentInitiationStatus.SuspendedCreatePurse;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
|
newStatus = PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
|
newStatus = PeerPullPaymentInitiationStatus.SuspendedWithdrawing;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
|
newStatus = PeerPullPaymentInitiationStatus.SuspendedReady;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
|
newStatus =
|
|
PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.Done:
|
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
|
case PeerPullPaymentInitiationStatus.Aborted:
|
|
case PeerPullPaymentInitiationStatus.Failed:
|
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
|
break;
|
|
default:
|
|
assertUnreachable(pullCreditRec.status);
|
|
}
|
|
if (newStatus != null) {
|
|
const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
|
pullCreditRec.status = newStatus;
|
|
const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
|
await tx.peerPullPaymentInitiations.put(pullCreditRec);
|
|
return {
|
|
oldTxState,
|
|
newTxState,
|
|
};
|
|
}
|
|
return undefined;
|
|
});
|
|
notifyTransition(ws, transactionId, transitionInfo);
|
|
}
|
|
|
|
export async function abortPeerPullCreditTransaction(
|
|
ws: InternalWalletState,
|
|
pursePub: string,
|
|
) {
|
|
const taskId = constructTaskIdentifier({
|
|
tag: PendingTaskType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
stopLongpolling(ws, taskId);
|
|
const transitionInfo = await ws.db
|
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
|
.runReadWrite(async (tx) => {
|
|
const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub);
|
|
if (!pullCreditRec) {
|
|
logger.warn(`peer pull credit ${pursePub} not found`);
|
|
return;
|
|
}
|
|
let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined;
|
|
switch (pullCreditRec.status) {
|
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
|
newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
|
throw Error("can't abort anymore");
|
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
|
newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.Done:
|
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
|
case PeerPullPaymentInitiationStatus.Aborted:
|
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
|
case PeerPullPaymentInitiationStatus.Failed:
|
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
|
break;
|
|
default:
|
|
assertUnreachable(pullCreditRec.status);
|
|
}
|
|
if (newStatus != null) {
|
|
const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
|
pullCreditRec.status = newStatus;
|
|
const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
|
await tx.peerPullPaymentInitiations.put(pullCreditRec);
|
|
return {
|
|
oldTxState,
|
|
newTxState,
|
|
};
|
|
}
|
|
return undefined;
|
|
});
|
|
notifyTransition(ws, transactionId, transitionInfo);
|
|
}
|
|
|
|
export async function failPeerPullCreditTransaction(
|
|
ws: InternalWalletState,
|
|
pursePub: string,
|
|
) {
|
|
const taskId = constructTaskIdentifier({
|
|
tag: PendingTaskType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
stopLongpolling(ws, taskId);
|
|
const transitionInfo = await ws.db
|
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
|
.runReadWrite(async (tx) => {
|
|
const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub);
|
|
if (!pullCreditRec) {
|
|
logger.warn(`peer pull credit ${pursePub} not found`);
|
|
return;
|
|
}
|
|
let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined;
|
|
switch (pullCreditRec.status) {
|
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
|
case PeerPullPaymentInitiationStatus.Done:
|
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
|
case PeerPullPaymentInitiationStatus.Aborted:
|
|
case PeerPullPaymentInitiationStatus.Failed:
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
|
newStatus = PeerPullPaymentInitiationStatus.Failed;
|
|
break;
|
|
default:
|
|
assertUnreachable(pullCreditRec.status);
|
|
}
|
|
if (newStatus != null) {
|
|
const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
|
pullCreditRec.status = newStatus;
|
|
const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
|
await tx.peerPullPaymentInitiations.put(pullCreditRec);
|
|
return {
|
|
oldTxState,
|
|
newTxState,
|
|
};
|
|
}
|
|
return undefined;
|
|
});
|
|
notifyTransition(ws, transactionId, transitionInfo);
|
|
}
|
|
|
|
export async function resumePeerPullCreditTransaction(
|
|
ws: InternalWalletState,
|
|
pursePub: string,
|
|
) {
|
|
const taskId = constructTaskIdentifier({
|
|
tag: PendingTaskType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
const transactionId = constructTransactionIdentifier({
|
|
tag: TransactionType.PeerPullCredit,
|
|
pursePub,
|
|
});
|
|
stopLongpolling(ws, taskId);
|
|
const transitionInfo = await ws.db
|
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
|
.runReadWrite(async (tx) => {
|
|
const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub);
|
|
if (!pullCreditRec) {
|
|
logger.warn(`peer pull credit ${pursePub} not found`);
|
|
return;
|
|
}
|
|
let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined;
|
|
switch (pullCreditRec.status) {
|
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
|
case PeerPullPaymentInitiationStatus.Done:
|
|
case PeerPullPaymentInitiationStatus.Failed:
|
|
case PeerPullPaymentInitiationStatus.Aborted:
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
|
newStatus = PeerPullPaymentInitiationStatus.PendingCreatePurse;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
|
newStatus = PeerPullPaymentInitiationStatus.PendingMergeKycRequired;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
|
newStatus = PeerPullPaymentInitiationStatus.PendingReady;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
|
newStatus = PeerPullPaymentInitiationStatus.PendingWithdrawing;
|
|
break;
|
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
|
newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse;
|
|
break;
|
|
default:
|
|
assertUnreachable(pullCreditRec.status);
|
|
}
|
|
if (newStatus != null) {
|
|
const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
|
pullCreditRec.status = newStatus;
|
|
const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
|
|
await tx.peerPullPaymentInitiations.put(pullCreditRec);
|
|
return {
|
|
oldTxState,
|
|
newTxState,
|
|
};
|
|
}
|
|
return undefined;
|
|
});
|
|
ws.workAvailable.trigger();
|
|
notifyTransition(ws, transactionId, transitionInfo);
|
|
}
|
|
|
|
export function computePeerPullCreditTransactionState(
|
|
pullCreditRecord: PeerPullPaymentInitiationRecord,
|
|
): TransactionState {
|
|
switch (pullCreditRecord.status) {
|
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
|
return {
|
|
major: TransactionMajorState.Pending,
|
|
minor: TransactionMinorState.CreatePurse,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
|
return {
|
|
major: TransactionMajorState.Pending,
|
|
minor: TransactionMinorState.MergeKycRequired,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
|
return {
|
|
major: TransactionMajorState.Pending,
|
|
minor: TransactionMinorState.Ready,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.Done:
|
|
return {
|
|
major: TransactionMajorState.Done,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
|
return {
|
|
major: TransactionMajorState.Pending,
|
|
minor: TransactionMinorState.Withdraw,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
|
return {
|
|
major: TransactionMajorState.Suspended,
|
|
minor: TransactionMinorState.CreatePurse,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
|
return {
|
|
major: TransactionMajorState.Suspended,
|
|
minor: TransactionMinorState.Ready,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
|
return {
|
|
major: TransactionMajorState.Pending,
|
|
minor: TransactionMinorState.Withdraw,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
|
return {
|
|
major: TransactionMajorState.Suspended,
|
|
minor: TransactionMinorState.MergeKycRequired,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.Aborted:
|
|
return {
|
|
major: TransactionMajorState.Aborted,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
|
return {
|
|
major: TransactionMajorState.Aborting,
|
|
minor: TransactionMinorState.DeletePurse,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.Failed:
|
|
return {
|
|
major: TransactionMajorState.Failed,
|
|
};
|
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
|
return {
|
|
major: TransactionMajorState.Aborting,
|
|
minor: TransactionMinorState.DeletePurse,
|
|
};
|
|
}
|
|
}
|
|
|
|
export function computePeerPullCreditTransactionActions(
|
|
pullCreditRecord: PeerPullPaymentInitiationRecord,
|
|
): TransactionAction[] {
|
|
switch (pullCreditRecord.status) {
|
|
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
|
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
|
case PeerPullPaymentInitiationStatus.PendingReady:
|
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
|
case PeerPullPaymentInitiationStatus.Done:
|
|
return [TransactionAction.Delete];
|
|
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
|
return [TransactionAction.Abort, TransactionAction.Suspend];
|
|
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
|
return [TransactionAction.Resume, TransactionAction.Abort];
|
|
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
|
return [TransactionAction.Abort, TransactionAction.Resume];
|
|
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
|
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
|
case PeerPullPaymentInitiationStatus.Aborted:
|
|
return [TransactionAction.Delete];
|
|
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
|
return [TransactionAction.Suspend, TransactionAction.Fail];
|
|
case PeerPullPaymentInitiationStatus.Failed:
|
|
return [TransactionAction.Delete];
|
|
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
|
}
|
|
}
|