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

1144 lines
38 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,
NotificationType,
TalerErrorCode,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
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 {
LongpollResult,
TaskRunResult,
TaskRunResultType,
constructTaskIdentifier,
runLongpollAsync,
} from "./common.js";
import {
codecForExchangePurseStatus,
getMergeReserveInfo,
talerPaytoFromExchangeReserve,
} from "./pay-peer-common.js";
import {
constructTransactionIdentifier,
notifyTransition,
stopLongpolling,
} from "./transactions.js";
import {
getExchangeWithdrawalInfo,
internalCreateWithdrawalGroup,
} 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);
}
}
const depositTimestamp = result.response.deposit_timestamp;
if (!depositTimestamp || TalerProtocolTimestamp.isNever(depositTimestamp)) {
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<TaskRunResult> {
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: TaskRunResultType.Longpoll,
};
}
async function processPeerPullCreditAbortingDeletePurse(
ws: InternalWalletState,
peerPullIni: PeerPullPaymentInitiationRecord,
): Promise<TaskRunResult> {
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 TaskRunResult.pending();
}
async function handlePeerPullCreditWithdrawing(
ws: InternalWalletState,
pullIni: PeerPullPaymentInitiationRecord,
): Promise<TaskRunResult> {
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 TaskRunResult.finished();
} else {
// FIXME: Return indicator that we depend on the other operation!
return TaskRunResult.pending();
}
}
async function handlePeerPullCreditCreatePurse(
ws: InternalWalletState,
pullIni: PeerPullPaymentInitiationRecord,
): Promise<TaskRunResult> {
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 TaskRunResult.finished();
}
export async function processPeerPullCredit(
ws: InternalWalletState,
pursePub: string,
): Promise<TaskRunResult> {
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: TaskRunResultType.Longpoll,
};
}
logger.trace(`processing ${retryTag}, status=${pullIni.status}`);
switch (pullIni.status) {
case PeerPullPaymentInitiationStatus.Done: {
return TaskRunResult.finished();
}
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: TaskRunResultType.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 TaskRunResult.finished();
}
async function processPeerPullCreditKycRequired(
ws: InternalWalletState,
peerIni: PeerPullPaymentInitiationRecord,
kycPending: WalletKycUuid,
): Promise<TaskRunResult> {
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 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.peerPullPaymentInitiations])
.runReadWrite(async (tx) => {
const peerInc = await tx.peerPullPaymentInitiations.get(pursePub);
if (!peerInc) {
return {
transitionInfo: undefined,
result: TaskRunResult.finished(),
};
}
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: 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 TaskRunResult.pending();
} 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`);
let numCoins = 0;
for (let i = 0; i < wi.selectedDenoms.selectedDenoms.length; i++) {
numCoins += wi.selectedDenoms.selectedDenoms[i].count;
}
return {
exchangeBaseUrl: exchangeUrl,
amountEffective: wi.withdrawalAmountEffective,
amountRaw: req.amount,
numCoins,
};
}
/**
* 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,
});
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullCredit,
pursePub: pursePair.pub,
});
// The pending-incoming balance has changed.
ws.notify({ type: NotificationType.BalanceChange });
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];
}
}