wallet-core/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts

1042 lines
34 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 {
Amounts,
CheckPeerPushDebitRequest,
CheckPeerPushDebitResponse,
CoinRefreshRequest,
ContractTermsUtil,
HttpStatusCode,
InitiatePeerPushDebitRequest,
InitiatePeerPushDebitResponse,
Logger,
NotificationType,
RefreshReason,
TalerError,
TalerErrorCode,
TalerPreciseTimestamp,
TalerProtocolViolationError,
TalerUriAction,
TransactionAction,
TransactionMajorState,
TransactionMinorState,
TransactionState,
TransactionType,
decodeCrock,
encodeCrock,
getRandomBytes,
hash,
j2s,
stringifyTalerUri,
} from "@gnu-taler/taler-util";
import {
HttpResponse,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import { EncryptContractRequest } from "../crypto/cryptoTypes.js";
import {
PeerPushPaymentInitiationRecord,
PeerPushPaymentInitiationStatus,
RefreshOperationStatus,
createRefreshGroup,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
import {
OperationAttemptResult,
OperationAttemptResultType,
constructTaskIdentifier,
runLongpollAsync,
spendCoins,
} from "./common.js";
import {
PeerCoinRepair,
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
queryCoinInfosForSelection,
selectPeerCoins,
} from "./pay-peer-common.js";
import {
constructTransactionIdentifier,
notifyTransition,
stopLongpolling,
} from "./transactions.js";
const logger = new Logger("pay-peer-push-debit.ts");
export async function checkPeerPushDebit(
ws: InternalWalletState,
req: CheckPeerPushDebitRequest,
): Promise<CheckPeerPushDebitResponse> {
const instructedAmount = Amounts.parseOrThrow(req.amount);
const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
if (coinSelRes.type === "failure") {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
{
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
);
}
const totalAmount = await getTotalPeerPaymentCost(
ws,
coinSelRes.result.coins,
);
return {
amountEffective: Amounts.stringify(totalAmount),
amountRaw: req.amount,
};
}
async function handlePurseCreationConflict(
ws: InternalWalletState,
peerPushInitiation: PeerPushPaymentInitiationRecord,
resp: HttpResponse,
): Promise<OperationAttemptResult> {
const pursePub = peerPushInitiation.pursePub;
const errResp = await readTalerErrorResponse(resp);
if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
await failPeerPushDebitTransaction(ws, pursePub);
return OperationAttemptResult.finishedEmpty();
}
// FIXME: Properly parse!
const brokenCoinPub = (errResp as any).coin_pub;
logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
if (!brokenCoinPub) {
// FIXME: Details!
throw new TalerProtocolViolationError();
}
const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
const sel = peerPushInitiation.coinSel;
const repair: PeerCoinRepair = {
coinPubs: [],
contribs: [],
exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
};
for (let i = 0; i < sel.coinPubs.length; i++) {
if (sel.coinPubs[i] != brokenCoinPub) {
repair.coinPubs.push(sel.coinPubs[i]);
repair.contribs.push(Amounts.parseOrThrow(sel.contributions[i]));
}
}
const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair });
if (coinSelRes.type == "failure") {
// FIXME: Details!
throw Error(
"insufficient balance to re-select coins to repair double spending",
);
}
await ws.db
.mktx((x) => [x.peerPushPaymentInitiations])
.runReadWrite(async (tx) => {
const myPpi = await tx.peerPushPaymentInitiations.get(
peerPushInitiation.pursePub,
);
if (!myPpi) {
return;
}
switch (myPpi.status) {
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: {
const sel = coinSelRes.result;
myPpi.coinSel = {
coinPubs: sel.coins.map((x) => x.coinPub),
contributions: sel.coins.map((x) => x.contribution),
};
break;
}
default:
return;
}
await tx.peerPushPaymentInitiations.put(myPpi);
});
return OperationAttemptResult.finishedEmpty();
}
async function processPeerPushDebitCreateReserve(
ws: InternalWalletState,
peerPushInitiation: PeerPushPaymentInitiationRecord,
): Promise<OperationAttemptResult> {
logger.info("processing peer-push-debit pending(create-reserve)");
const pursePub = peerPushInitiation.pursePub;
const purseExpiration = peerPushInitiation.purseExpiration;
const hContractTerms = peerPushInitiation.contractTermsHash;
const purseSigResp = await ws.cryptoApi.signPurseCreation({
hContractTerms,
mergePub: peerPushInitiation.mergePub,
minAge: 0,
purseAmount: peerPushInitiation.amount,
purseExpiration,
pursePriv: peerPushInitiation.pursePriv,
});
const coins = await queryCoinInfosForSelection(
ws,
peerPushInitiation.coinSel,
);
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
pursePub: peerPushInitiation.pursePub,
coins,
});
const encryptContractRequest: EncryptContractRequest = {
contractTerms: peerPushInitiation.contractTerms,
mergePriv: peerPushInitiation.mergePriv,
pursePriv: peerPushInitiation.pursePriv,
pursePub: peerPushInitiation.pursePub,
contractPriv: peerPushInitiation.contractPriv,
contractPub: peerPushInitiation.contractPub,
nonce: peerPushInitiation.contractEncNonce,
};
logger.info(`encrypt contract request: ${j2s(encryptContractRequest)}`);
const econtractResp = await ws.cryptoApi.encryptContractForMerge(
encryptContractRequest,
);
const econtractHash = encodeCrock(
hash(decodeCrock(econtractResp.econtract.econtract)),
);
logger.info(`econtract hash: ${econtractHash}`);
const createPurseUrl = new URL(
`purses/${peerPushInitiation.pursePub}/create`,
peerPushInitiation.exchangeBaseUrl,
);
const reqBody = {
amount: peerPushInitiation.amount,
merge_pub: peerPushInitiation.mergePub,
purse_sig: purseSigResp.sig,
h_contract_terms: hContractTerms,
purse_expiration: purseExpiration,
deposits: depositSigsResp.deposits,
min_age: 0,
econtract: econtractResp.econtract,
};
logger.info(`request body: ${j2s(reqBody)}`);
const httpResp = await ws.http.fetch(createPurseUrl.href, {
method: "POST",
body: reqBody,
});
{
const resp = await httpResp.json();
logger.info(`resp: ${j2s(resp)}`);
}
switch (httpResp.status) {
case HttpStatusCode.Ok:
break;
case HttpStatusCode.Forbidden: {
// FIXME: Store this error!
await failPeerPushDebitTransaction(ws, pursePub);
return OperationAttemptResult.finishedEmpty();
}
case HttpStatusCode.Conflict: {
// Handle double-spending
return handlePurseCreationConflict(ws, peerPushInitiation, httpResp);
}
default: {
const errResp = await readTalerErrorResponse(httpResp);
return {
type: OperationAttemptResultType.Error,
errorDetail: errResp,
};
}
}
if (httpResp.status !== HttpStatusCode.Ok) {
// FIXME: do proper error reporting
throw Error("got error response from exchange");
}
await transitionPeerPushDebitTransaction(ws, pursePub, {
stFrom: PeerPushPaymentInitiationStatus.PendingCreatePurse,
stTo: PeerPushPaymentInitiationStatus.PendingReady,
});
return OperationAttemptResult.finishedEmpty();
}
async function processPeerPushDebitAbortingDeletePurse(
ws: InternalWalletState,
peerPushInitiation: PeerPushPaymentInitiationRecord,
): Promise<OperationAttemptResult> {
const { pursePub, pursePriv } = peerPushInitiation;
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub,
});
const sigResp = await ws.cryptoApi.signDeletePurse({
pursePriv,
});
const purseUrl = new URL(
`purses/${pursePub}`,
peerPushInitiation.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.peerPushPaymentInitiations,
x.refreshGroups,
x.denominations,
x.coinAvailability,
x.coins,
])
.runReadWrite(async (tx) => {
const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub);
if (!ppiRec) {
return undefined;
}
if (
ppiRec.status !== PeerPushPaymentInitiationStatus.AbortingDeletePurse
) {
return undefined;
}
const currency = Amounts.currencyOf(ppiRec.amount);
const oldTxState = computePeerPushDebitTransactionState(ppiRec);
const coinPubs: CoinRefreshRequest[] = [];
for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
coinPubs.push({
amount: ppiRec.coinSel.contributions[i],
coinPub: ppiRec.coinSel.coinPubs[i],
});
}
const refresh = await createRefreshGroup(
ws,
tx,
currency,
coinPubs,
RefreshReason.AbortPeerPushDebit,
);
ppiRec.status = PeerPushPaymentInitiationStatus.AbortingRefresh;
ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
await tx.peerPushPaymentInitiations.put(ppiRec);
const newTxState = computePeerPushDebitTransactionState(ppiRec);
return {
oldTxState,
newTxState,
};
});
notifyTransition(ws, transactionId, transitionInfo);
return OperationAttemptResult.pendingEmpty();
}
interface SimpleTransition {
stFrom: PeerPushPaymentInitiationStatus;
stTo: PeerPushPaymentInitiationStatus;
}
async function transitionPeerPushDebitTransaction(
ws: InternalWalletState,
pursePub: string,
transitionSpec: SimpleTransition,
): Promise<void> {
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub,
});
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushPaymentInitiations])
.runReadWrite(async (tx) => {
const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub);
if (!ppiRec) {
return undefined;
}
if (ppiRec.status !== transitionSpec.stFrom) {
return undefined;
}
const oldTxState = computePeerPushDebitTransactionState(ppiRec);
ppiRec.status = transitionSpec.stTo;
await tx.peerPushPaymentInitiations.put(ppiRec);
const newTxState = computePeerPushDebitTransactionState(ppiRec);
return {
oldTxState,
newTxState,
};
});
notifyTransition(ws, transactionId, transitionInfo);
}
async function processPeerPushDebitAbortingRefresh(
ws: InternalWalletState,
peerPushInitiation: PeerPushPaymentInitiationRecord,
): Promise<OperationAttemptResult> {
const pursePub = peerPushInitiation.pursePub;
const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId;
checkLogicInvariant(!!abortRefreshGroupId);
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub: peerPushInitiation.pursePub,
});
const transitionInfo = await ws.db
.mktx((x) => [x.refreshGroups, x.peerPushPaymentInitiations])
.runReadWrite(async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: PeerPushPaymentInitiationStatus | undefined;
if (!refreshGroup) {
// Maybe it got manually deleted? Means that we should
// just go into failed.
logger.warn("no aborting refresh group found for deposit group");
newOpState = PeerPushPaymentInitiationStatus.Failed;
} else {
if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
newOpState = PeerPushPaymentInitiationStatus.Aborted;
} else if (
refreshGroup.operationStatus === RefreshOperationStatus.Failed
) {
newOpState = PeerPushPaymentInitiationStatus.Failed;
}
}
if (newOpState) {
const newDg = await tx.peerPushPaymentInitiations.get(pursePub);
if (!newDg) {
return;
}
const oldTxState = computePeerPushDebitTransactionState(newDg);
newDg.status = newOpState;
const newTxState = computePeerPushDebitTransactionState(newDg);
await tx.peerPushPaymentInitiations.put(newDg);
return { oldTxState, newTxState };
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
// FIXME: Shouldn't this be finished in some cases?!
return OperationAttemptResult.pendingEmpty();
}
/**
* Process the "pending(ready)" state of a peer-push-debit transaction.
*/
async function processPeerPushDebitReady(
ws: InternalWalletState,
peerPushInitiation: PeerPushPaymentInitiationRecord,
): Promise<OperationAttemptResult> {
logger.info("processing peer-push-debit pending(ready)");
const pursePub = peerPushInitiation.pursePub;
const retryTag = constructTaskIdentifier({
tag: PendingTaskType.PeerPushDebit,
pursePub,
});
runLongpollAsync(ws, retryTag, async (ct) => {
const mergeUrl = new URL(
`purses/${pursePub}/merge`,
peerPushInitiation.exchangeBaseUrl,
);
mergeUrl.searchParams.set("timeout_ms", "30000");
logger.info(`long-polling on purse status at ${mergeUrl.href}`);
const resp = await ws.http.fetch(mergeUrl.href, {
// timeout: getReserveRequestTimeout(withdrawalGroup),
cancellationToken: ct,
});
if (resp.status === HttpStatusCode.Ok) {
const purseStatus = await readSuccessResponseJsonOrThrow(
resp,
codecForExchangePurseStatus(),
);
logger.info(`got purse status ${purseStatus}`);
if (purseStatus.merge_timestamp) {
await transitionPeerPushDebitTransaction(
ws,
peerPushInitiation.pursePub,
{
stFrom: PeerPushPaymentInitiationStatus.PendingReady,
stTo: PeerPushPaymentInitiationStatus.Done,
},
);
return {
ready: true,
};
}
} else if (resp.status === HttpStatusCode.Gone) {
await transitionPeerPushDebitTransaction(
ws,
peerPushInitiation.pursePub,
{
stFrom: PeerPushPaymentInitiationStatus.PendingReady,
stTo: PeerPushPaymentInitiationStatus.Expired,
},
);
return {
ready: true,
};
}
return {
ready: false,
};
});
logger.trace(
"returning early from peer-push-debit for long-polling in background",
);
return {
type: OperationAttemptResultType.Longpoll,
};
}
export async function processPeerPushDebit(
ws: InternalWalletState,
pursePub: string,
): Promise<OperationAttemptResult> {
const peerPushInitiation = await ws.db
.mktx((x) => [x.peerPushPaymentInitiations])
.runReadOnly(async (tx) => {
return tx.peerPushPaymentInitiations.get(pursePub);
});
if (!peerPushInitiation) {
throw Error("peer push payment not found");
}
const retryTag = constructTaskIdentifier({
tag: PendingTaskType.PeerPushDebit,
pursePub,
});
// We're already running!
if (ws.activeLongpoll[retryTag]) {
logger.info("peer-push-debit task already in long-polling, returning!");
return {
type: OperationAttemptResultType.Longpoll,
};
}
switch (peerPushInitiation.status) {
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
return processPeerPushDebitCreateReserve(ws, peerPushInitiation);
case PeerPushPaymentInitiationStatus.PendingReady:
return processPeerPushDebitReady(ws, peerPushInitiation);
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
return processPeerPushDebitAbortingDeletePurse(ws, peerPushInitiation);
case PeerPushPaymentInitiationStatus.AbortingRefresh:
return processPeerPushDebitAbortingRefresh(ws, peerPushInitiation);
default: {
const txState = computePeerPushDebitTransactionState(peerPushInitiation);
logger.warn(
`not processing peer-push-debit transaction in state ${j2s(txState)}`,
);
}
}
return {
type: OperationAttemptResultType.Finished,
result: undefined,
};
}
/**
* Initiate sending a peer-to-peer push payment.
*/
export async function initiatePeerPushDebit(
ws: InternalWalletState,
req: InitiatePeerPushDebitRequest,
): Promise<InitiatePeerPushDebitResponse> {
const instructedAmount = Amounts.parseOrThrow(
req.partialContractTerms.amount,
);
const purseExpiration = req.partialContractTerms.purse_expiration;
const contractTerms = req.partialContractTerms;
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({});
const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
if (coinSelRes.type !== "success") {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
{
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
);
}
const sel = coinSelRes.result;
logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
const totalAmount = await getTotalPeerPaymentCost(
ws,
coinSelRes.result.coins,
);
const pursePub = pursePair.pub;
const transactionId = constructTaskIdentifier({
tag: PendingTaskType.PeerPushDebit,
pursePub,
});
const contractEncNonce = encodeCrock(getRandomBytes(24));
const transitionInfo = await ws.db
.mktx((x) => [
x.exchanges,
x.contractTerms,
x.coins,
x.coinAvailability,
x.denominations,
x.refreshGroups,
x.peerPushPaymentInitiations,
])
.runReadWrite(async (tx) => {
// FIXME: Instead of directly doing a spendCoin here,
// we might want to mark the coins as used and spend them
// after we've been able to create the purse.
await spendCoins(ws, tx, {
// allocationId: `txn:peer-push-debit:${pursePair.pub}`,
allocationId: constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub: pursePair.pub,
}),
coinPubs: sel.coins.map((x) => x.coinPub),
contributions: sel.coins.map((x) =>
Amounts.parseOrThrow(x.contribution),
),
refreshReason: RefreshReason.PayPeerPush,
});
const ppi: PeerPushPaymentInitiationRecord = {
amount: Amounts.stringify(instructedAmount),
contractPriv: contractKeyPair.priv,
contractPub: contractKeyPair.pub,
contractTermsHash: hContractTerms,
exchangeBaseUrl: sel.exchangeBaseUrl,
mergePriv: mergePair.priv,
mergePub: mergePair.pub,
purseExpiration: purseExpiration,
pursePriv: pursePair.priv,
pursePub: pursePair.pub,
timestampCreated: TalerPreciseTimestamp.now(),
status: PeerPushPaymentInitiationStatus.PendingCreatePurse,
contractTerms: contractTerms,
contractEncNonce,
coinSel: {
coinPubs: sel.coins.map((x) => x.coinPub),
contributions: sel.coins.map((x) => x.contribution),
},
totalCost: Amounts.stringify(totalAmount),
};
await tx.peerPushPaymentInitiations.add(ppi);
await tx.contractTerms.put({
h: hContractTerms,
contractTermsRaw: contractTerms,
});
const newTxState = computePeerPushDebitTransactionState(ppi);
return {
oldTxState: { major: TransactionMajorState.None },
newTxState,
};
});
notifyTransition(ws, transactionId, transitionInfo);
ws.notify({ type: NotificationType.BalanceChange });
return {
contractPriv: contractKeyPair.priv,
mergePriv: mergePair.priv,
pursePub: pursePair.pub,
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
talerUri: stringifyTalerUri({
type: TalerUriAction.PayPush,
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
contractPriv: contractKeyPair.priv,
}),
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub: pursePair.pub,
}),
};
}
export function computePeerPushDebitTransactionActions(
ppiRecord: PeerPushPaymentInitiationRecord,
): TransactionAction[] {
switch (ppiRecord.status) {
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
return [TransactionAction.Abort, TransactionAction.Suspend];
case PeerPushPaymentInitiationStatus.PendingReady:
return [TransactionAction.Abort, TransactionAction.Suspend];
case PeerPushPaymentInitiationStatus.Aborted:
return [TransactionAction.Delete];
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
return [TransactionAction.Suspend, TransactionAction.Fail];
case PeerPushPaymentInitiationStatus.AbortingRefresh:
return [TransactionAction.Suspend, TransactionAction.Fail];
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
return [TransactionAction.Resume, TransactionAction.Fail];
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
return [TransactionAction.Resume, TransactionAction.Fail];
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPushPaymentInitiationStatus.SuspendedReady:
return [TransactionAction.Suspend, TransactionAction.Abort];
case PeerPushPaymentInitiationStatus.Done:
return [TransactionAction.Delete];
case PeerPushPaymentInitiationStatus.Expired:
return [TransactionAction.Delete];
case PeerPushPaymentInitiationStatus.Failed:
return [TransactionAction.Delete];
}
}
export async function abortPeerPushDebitTransaction(
ws: InternalWalletState,
pursePub: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPushDebit,
pursePub,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushPaymentInitiations])
.runReadWrite(async (tx) => {
const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
if (!pushDebitRec) {
logger.warn(`peer push debit ${pursePub} not found`);
return;
}
let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
switch (pushDebitRec.status) {
case PeerPushPaymentInitiationStatus.PendingReady:
case PeerPushPaymentInitiationStatus.SuspendedReady:
newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse;
break;
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
// Network request might already be in-flight!
newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse;
break;
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
case PeerPushPaymentInitiationStatus.AbortingRefresh:
case PeerPushPaymentInitiationStatus.Done:
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
case PeerPushPaymentInitiationStatus.Aborted:
case PeerPushPaymentInitiationStatus.Expired:
case PeerPushPaymentInitiationStatus.Failed:
// Do nothing
break;
default:
assertUnreachable(pushDebitRec.status);
}
if (newStatus != null) {
const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
pushDebitRec.status = newStatus;
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
await tx.peerPushPaymentInitiations.put(pushDebitRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
}
export async function failPeerPushDebitTransaction(
ws: InternalWalletState,
pursePub: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPushDebit,
pursePub,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushPaymentInitiations])
.runReadWrite(async (tx) => {
const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
if (!pushDebitRec) {
logger.warn(`peer push debit ${pursePub} not found`);
return;
}
let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
switch (pushDebitRec.status) {
case PeerPushPaymentInitiationStatus.AbortingRefresh:
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
// FIXME: What to do about the refresh group?
newStatus = PeerPushPaymentInitiationStatus.Failed;
break;
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
case PeerPushPaymentInitiationStatus.PendingReady:
case PeerPushPaymentInitiationStatus.SuspendedReady:
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
newStatus = PeerPushPaymentInitiationStatus.Failed;
break;
case PeerPushPaymentInitiationStatus.Done:
case PeerPushPaymentInitiationStatus.Aborted:
case PeerPushPaymentInitiationStatus.Failed:
case PeerPushPaymentInitiationStatus.Expired:
// Do nothing
break;
default:
assertUnreachable(pushDebitRec.status);
}
if (newStatus != null) {
const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
pushDebitRec.status = newStatus;
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
await tx.peerPushPaymentInitiations.put(pushDebitRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
}
export async function suspendPeerPushDebitTransaction(
ws: InternalWalletState,
pursePub: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPushDebit,
pursePub,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushPaymentInitiations])
.runReadWrite(async (tx) => {
const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
if (!pushDebitRec) {
logger.warn(`peer push debit ${pursePub} not found`);
return;
}
let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
switch (pushDebitRec.status) {
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
newStatus = PeerPushPaymentInitiationStatus.SuspendedCreatePurse;
break;
case PeerPushPaymentInitiationStatus.AbortingRefresh:
newStatus = PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh;
break;
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
newStatus =
PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse;
break;
case PeerPushPaymentInitiationStatus.PendingReady:
newStatus = PeerPushPaymentInitiationStatus.SuspendedReady;
break;
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
case PeerPushPaymentInitiationStatus.SuspendedReady:
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
case PeerPushPaymentInitiationStatus.Done:
case PeerPushPaymentInitiationStatus.Aborted:
case PeerPushPaymentInitiationStatus.Failed:
case PeerPushPaymentInitiationStatus.Expired:
// Do nothing
break;
default:
assertUnreachable(pushDebitRec.status);
}
if (newStatus != null) {
const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
pushDebitRec.status = newStatus;
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
await tx.peerPushPaymentInitiations.put(pushDebitRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
notifyTransition(ws, transactionId, transitionInfo);
}
export async function resumePeerPushDebitTransaction(
ws: InternalWalletState,
pursePub: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.PeerPushDebit,
pursePub,
});
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub,
});
stopLongpolling(ws, taskId);
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushPaymentInitiations])
.runReadWrite(async (tx) => {
const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
if (!pushDebitRec) {
logger.warn(`peer push debit ${pursePub} not found`);
return;
}
let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
switch (pushDebitRec.status) {
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse;
break;
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
newStatus = PeerPushPaymentInitiationStatus.AbortingRefresh;
break;
case PeerPushPaymentInitiationStatus.SuspendedReady:
newStatus = PeerPushPaymentInitiationStatus.PendingReady;
break;
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
newStatus = PeerPushPaymentInitiationStatus.PendingCreatePurse;
break;
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
case PeerPushPaymentInitiationStatus.AbortingRefresh:
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
case PeerPushPaymentInitiationStatus.PendingReady:
case PeerPushPaymentInitiationStatus.Done:
case PeerPushPaymentInitiationStatus.Aborted:
case PeerPushPaymentInitiationStatus.Failed:
case PeerPushPaymentInitiationStatus.Expired:
// Do nothing
break;
default:
assertUnreachable(pushDebitRec.status);
}
if (newStatus != null) {
const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
pushDebitRec.status = newStatus;
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
await tx.peerPushPaymentInitiations.put(pushDebitRec);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
ws.workAvailable.trigger();
notifyTransition(ws, transactionId, transitionInfo);
}
export function computePeerPushDebitTransactionState(
ppiRecord: PeerPushPaymentInitiationRecord,
): TransactionState {
switch (ppiRecord.status) {
case PeerPushPaymentInitiationStatus.PendingCreatePurse:
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.CreatePurse,
};
case PeerPushPaymentInitiationStatus.PendingReady:
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Ready,
};
case PeerPushPaymentInitiationStatus.Aborted:
return {
major: TransactionMajorState.Aborted,
};
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
return {
major: TransactionMajorState.Aborting,
minor: TransactionMinorState.DeletePurse,
};
case PeerPushPaymentInitiationStatus.AbortingRefresh:
return {
major: TransactionMajorState.Aborting,
minor: TransactionMinorState.Refresh,
};
case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
return {
major: TransactionMajorState.SuspendedAborting,
minor: TransactionMinorState.DeletePurse,
};
case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
return {
major: TransactionMajorState.SuspendedAborting,
minor: TransactionMinorState.Refresh,
};
case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
return {
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.CreatePurse,
};
case PeerPushPaymentInitiationStatus.SuspendedReady:
return {
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.Ready,
};
case PeerPushPaymentInitiationStatus.Done:
return {
major: TransactionMajorState.Done,
};
case PeerPushPaymentInitiationStatus.Failed:
return {
major: TransactionMajorState.Failed,
};
case PeerPushPaymentInitiationStatus.Expired:
return {
major: TransactionMajorState.Expired,
};
}
}