wallet-core: handle more p2p abort cases nicely

This commit is contained in:
Florian Dold 2023-06-05 17:58:20 +02:00
parent 6e7c88a620
commit 9fca44893a
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
11 changed files with 385 additions and 100 deletions

View File

@ -45,7 +45,7 @@ export interface HttpResponse {
export const DEFAULT_REQUEST_TIMEOUT_MS = 60000; export const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
export interface HttpRequestOptions { export interface HttpRequestOptions {
method?: "POST" | "PUT" | "GET"; method?: "POST" | "PUT" | "GET" | "DELETE";
headers?: { [name: string]: string }; headers?: { [name: string]: string };
/** /**

View File

@ -958,6 +958,7 @@ export enum TalerSignaturePurpose {
WALLET_PURSE_MERGE = 1213, WALLET_PURSE_MERGE = 1213,
WALLET_ACCOUNT_MERGE = 1214, WALLET_ACCOUNT_MERGE = 1214,
WALLET_PURSE_ECONTRACT = 1216, WALLET_PURSE_ECONTRACT = 1216,
WALLET_PURSE_DELETE = 1220,
EXCHANGE_CONFIRM_RECOUP = 1039, EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041, EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
ANASTASIS_POLICY_UPLOAD = 1400, ANASTASIS_POLICY_UPLOAD = 1400,

View File

@ -83,6 +83,7 @@ export enum TransactionMajorState {
Dialog = "dialog", Dialog = "dialog",
SuspendedAborting = "suspended-aborting", SuspendedAborting = "suspended-aborting",
Failed = "failed", Failed = "failed",
Expired = "expired",
// Only used for the notification, never in the transaction history // Only used for the notification, never in the transaction history
Deleted = "deleted", Deleted = "deleted",
} }

View File

@ -734,6 +734,7 @@ export enum RefreshReason {
Refund = "refund", Refund = "refund",
AbortPay = "abort-pay", AbortPay = "abort-pay",
AbortDeposit = "abort-deposit", AbortDeposit = "abort-deposit",
AbortPeerPushDebit = "abort-peer-push-debit",
Recoup = "recoup", Recoup = "recoup",
BackupRestored = "backup-restored", BackupRestored = "backup-restored",
Scheduled = "scheduled", Scheduled = "scheduled",

View File

@ -106,6 +106,8 @@ import {
EncryptContractRequest, EncryptContractRequest,
EncryptContractResponse, EncryptContractResponse,
EncryptedContract, EncryptedContract,
SignDeletePurseRequest,
SignDeletePurseResponse,
SignPurseMergeRequest, SignPurseMergeRequest,
SignPurseMergeResponse, SignPurseMergeResponse,
SignRefundRequest, SignRefundRequest,
@ -240,6 +242,8 @@ export interface TalerCryptoInterface {
): Promise<SignReservePurseCreateResponse>; ): Promise<SignReservePurseCreateResponse>;
signRefund(req: SignRefundRequest): Promise<SignRefundResponse>; signRefund(req: SignRefundRequest): Promise<SignRefundResponse>;
signDeletePurse(req: SignDeletePurseRequest): Promise<SignDeletePurseResponse>;
} }
/** /**
@ -419,6 +423,11 @@ export const nullCrypto: TalerCryptoInterface = {
signRefund: function (req: SignRefundRequest): Promise<SignRefundResponse> { signRefund: function (req: SignRefundRequest): Promise<SignRefundResponse> {
throw new Error("Function not implemented."); throw new Error("Function not implemented.");
}, },
signDeletePurse: function (
req: SignDeletePurseRequest,
): Promise<SignDeletePurseResponse> {
throw new Error("Function not implemented.");
},
}; };
export type WithArg<X> = X extends (req: infer T) => infer R export type WithArg<X> = X extends (req: infer T) => infer R
@ -1671,6 +1680,21 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
sig: refundSigResp.sig, sig: refundSigResp.sig,
}; };
}, },
async signDeletePurse(
tci: TalerCryptoInterfaceR,
req: SignDeletePurseRequest,
): Promise<SignDeletePurseResponse> {
const deleteSigBlob = buildSigPS(
TalerSignaturePurpose.WALLET_PURSE_DELETE,
).build();
const sigResp = await tci.eddsaSign(tci, {
msg: encodeCrock(deleteSigBlob),
priv: req.pursePriv,
});
return {
sig: sigResp.sig,
}
},
}; };
function amountToBuffer(amount: AmountLike): Uint8Array { function amountToBuffer(amount: AmountLike): Uint8Array {

View File

@ -268,7 +268,14 @@ export interface SignRefundResponse {
sig: string; sig: string;
} }
export interface SignRefundResponse {}
export interface SignDeletePurseRequest {
pursePriv: string;
}
export interface SignDeletePurseResponse {
sig: EddsaSignatureString;
}
export interface SignReservePurseCreateRequest { export interface SignReservePurseCreateRequest {
mergeTimestamp: TalerProtocolTimestamp; mergeTimestamp: TalerProtocolTimestamp;

View File

@ -1787,6 +1787,7 @@ export enum PeerPushPaymentInitiationStatus {
Done = 50 /* DORMANT_START */, Done = 50 /* DORMANT_START */,
Aborted = 51, Aborted = 51,
Failed = 52, Failed = 52,
Expired = 53,
} }
export interface PeerPushPaymentCoinSelection { export interface PeerPushPaymentCoinSelection {
@ -1844,6 +1845,8 @@ export interface PeerPushPaymentInitiationRecord {
timestampCreated: TalerPreciseTimestamp; timestampCreated: TalerPreciseTimestamp;
abortRefreshGroupId?: string;
/** /**
* Status of the peer push payment initiation. * Status of the peer push payment initiation.
*/ */

View File

@ -83,7 +83,6 @@ import {
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { import {
checkWithdrawalKycStatus,
getExchangeWithdrawalInfo, getExchangeWithdrawalInfo,
internalCreateWithdrawalGroup, internalCreateWithdrawalGroup,
processWithdrawalGroup, processWithdrawalGroup,
@ -241,6 +240,62 @@ async function longpollKycStatus(
}; };
} }
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();
}
export async function processPeerPullCredit( export async function processPeerPullCredit(
ws: InternalWalletState, ws: InternalWalletState,
pursePub: string, pursePub: string,
@ -320,6 +375,8 @@ export async function processPeerPullCredit(
} }
case PeerPullPaymentInitiationStatus.PendingCreatePurse: case PeerPullPaymentInitiationStatus.PendingCreatePurse:
break; break;
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
return await processPeerPullCreditAbortingDeletePurse(ws, pullIni);
default: default:
throw Error(`unknown PeerPullPaymentInitiationStatus ${pullIni.status}`); throw Error(`unknown PeerPullPaymentInitiationStatus ${pullIni.status}`);
} }

View File

@ -15,55 +15,152 @@
*/ */
import { import {
ConfirmPeerPullDebitRequest,
AcceptPeerPullPaymentResponse, AcceptPeerPullPaymentResponse,
Amounts, Amounts,
j2s, ConfirmPeerPullDebitRequest,
TalerError, ExchangePurseDeposits,
TalerErrorCode,
TransactionType,
RefreshReason,
Logger, Logger,
PeerContractTerms, PeerContractTerms,
PreparePeerPullDebitRequest, PreparePeerPullDebitRequest,
PreparePeerPullDebitResponse, PreparePeerPullDebitResponse,
RefreshReason,
TalerError,
TalerErrorCode,
TalerPreciseTimestamp, TalerPreciseTimestamp,
TransactionAction,
TransactionMajorState,
TransactionMinorState,
TransactionState,
TransactionType,
codecForAny,
codecForExchangeGetContractResponse, codecForExchangeGetContractResponse,
codecForPeerContractTerms, codecForPeerContractTerms,
decodeCrock, decodeCrock,
eddsaGetPublic, eddsaGetPublic,
encodeCrock, encodeCrock,
getRandomBytes, getRandomBytes,
j2s,
parsePayPullUri, parsePayPullUri,
TransactionAction,
TransactionMajorState,
TransactionMinorState,
TransactionState,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { import {
InternalWalletState, InternalWalletState,
PeerPullDebitRecordStatus, PeerPullDebitRecordStatus,
PeerPullPaymentIncomingRecord, PeerPullPaymentIncomingRecord,
PendingTaskType, PendingTaskType,
} from "../index.js"; } from "../index.js";
import { TaskIdentifiers, constructTaskIdentifier } from "../util/retries.js"; import { assertUnreachable } from "../util/assertUnreachable.js";
import { spendCoins, runOperationWithErrorReporting } from "./common.js"; import {
OperationAttemptResult,
OperationAttemptResultType,
TaskIdentifiers,
constructTaskIdentifier,
} from "../util/retries.js";
import { runOperationWithErrorReporting, spendCoins } from "./common.js";
import { import {
codecForExchangePurseStatus, codecForExchangePurseStatus,
getTotalPeerPaymentCost, getTotalPeerPaymentCost,
queryCoinInfosForSelection,
selectPeerCoins, selectPeerCoins,
} from "./pay-peer-common.js"; } from "./pay-peer-common.js";
import { processPeerPullDebit } from "./pay-peer-push-credit.js";
import { import {
constructTransactionIdentifier, constructTransactionIdentifier,
notifyTransition, notifyTransition,
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { assertUnreachable } from "../util/assertUnreachable.js";
const logger = new Logger("pay-peer-pull-debit.ts"); const logger = new Logger("pay-peer-pull-debit.ts");
async function processPeerPullDebitPendingDeposit(
ws: InternalWalletState,
peerPullInc: PeerPullPaymentIncomingRecord,
): Promise<OperationAttemptResult> {
const peerPullPaymentIncomingId = peerPullInc.peerPullPaymentIncomingId;
const pursePub = peerPullInc.pursePub;
const coinSel = peerPullInc.coinSel;
if (!coinSel) {
throw Error("invalid state, no coins selected");
}
const coins = await queryCoinInfosForSelection(ws, coinSel);
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
pursePub: peerPullInc.pursePub,
coins,
});
const purseDepositUrl = new URL(
`purses/${pursePub}/deposit`,
peerPullInc.exchangeBaseUrl,
);
const depositPayload: ExchangePurseDeposits = {
deposits: depositSigsResp.deposits,
};
if (logger.shouldLogTrace()) {
logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
}
const httpResp = await ws.http.postJson(purseDepositUrl.href, depositPayload);
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
logger.trace(`purse deposit response: ${j2s(resp)}`);
await ws.db
.mktx((x) => [x.peerPullPaymentIncoming])
.runReadWrite(async (tx) => {
const pi = await tx.peerPullPaymentIncoming.get(
peerPullPaymentIncomingId,
);
if (!pi) {
throw Error("peer pull payment not found anymore");
}
if (pi.status === PeerPullDebitRecordStatus.PendingDeposit) {
pi.status = PeerPullDebitRecordStatus.DonePaid;
}
await tx.peerPullPaymentIncoming.put(pi);
});
return {
type: OperationAttemptResultType.Finished,
result: undefined,
};
}
async function processPeerPullDebitAbortingRefresh(
ws: InternalWalletState,
peerPullInc: PeerPullPaymentIncomingRecord,
): Promise<OperationAttemptResult> {
throw Error("not implemented");
}
export async function processPeerPullDebit(
ws: InternalWalletState,
peerPullPaymentIncomingId: string,
): Promise<OperationAttemptResult> {
const peerPullInc = await ws.db
.mktx((x) => [x.peerPullPaymentIncoming])
.runReadOnly(async (tx) => {
return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId);
});
if (!peerPullInc) {
throw Error("peer pull debit not found");
}
switch (peerPullInc.status) {
case PeerPullDebitRecordStatus.PendingDeposit:
return await processPeerPullDebitPendingDeposit(ws, peerPullInc);
case PeerPullDebitRecordStatus.AbortingRefresh:
return await processPeerPullDebitAbortingRefresh(ws, peerPullInc);
}
return {
type: OperationAttemptResultType.Finished,
result: undefined,
}
}
export async function confirmPeerPullDebit( export async function confirmPeerPullDebit(
ws: InternalWalletState, ws: InternalWalletState,
req: ConfirmPeerPullDebitRequest, req: ConfirmPeerPullDebitRequest,

View File

@ -553,76 +553,6 @@ export async function confirmPeerPushCredit(
}; };
} }
export async function processPeerPullDebit(
ws: InternalWalletState,
peerPullPaymentIncomingId: string,
): Promise<OperationAttemptResult> {
const peerPullInc = await ws.db
.mktx((x) => [x.peerPullPaymentIncoming])
.runReadOnly(async (tx) => {
return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId);
});
if (!peerPullInc) {
throw Error("peer pull debit not found");
}
if (peerPullInc.status === PeerPullDebitRecordStatus.PendingDeposit) {
const pursePub = peerPullInc.pursePub;
const coinSel = peerPullInc.coinSel;
if (!coinSel) {
throw Error("invalid state, no coins selected");
}
const coins = await queryCoinInfosForSelection(ws, coinSel);
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
pursePub: peerPullInc.pursePub,
coins,
});
const purseDepositUrl = new URL(
`purses/${pursePub}/deposit`,
peerPullInc.exchangeBaseUrl,
);
const depositPayload: ExchangePurseDeposits = {
deposits: depositSigsResp.deposits,
};
if (logger.shouldLogTrace()) {
logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
}
const httpResp = await ws.http.postJson(
purseDepositUrl.href,
depositPayload,
);
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
logger.trace(`purse deposit response: ${j2s(resp)}`);
}
await ws.db
.mktx((x) => [x.peerPullPaymentIncoming])
.runReadWrite(async (tx) => {
const pi = await tx.peerPullPaymentIncoming.get(
peerPullPaymentIncomingId,
);
if (!pi) {
throw Error("peer pull payment not found anymore");
}
if (pi.status === PeerPullDebitRecordStatus.PendingDeposit) {
pi.status = PeerPullDebitRecordStatus.DonePaid;
}
await tx.peerPullPaymentIncoming.put(pi);
});
return {
type: OperationAttemptResultType.Finished,
result: undefined,
};
}
export async function suspendPeerPushCreditTransaction( export async function suspendPeerPushCreditTransaction(
ws: InternalWalletState, ws: InternalWalletState,
peerPushPaymentIncomingId: string, peerPushPaymentIncomingId: string,

View File

@ -18,6 +18,7 @@ import {
Amounts, Amounts,
CheckPeerPushDebitRequest, CheckPeerPushDebitRequest,
CheckPeerPushDebitResponse, CheckPeerPushDebitResponse,
CoinRefreshRequest,
ContractTermsUtil, ContractTermsUtil,
HttpStatusCode, HttpStatusCode,
InitiatePeerPushDebitRequest, InitiatePeerPushDebitRequest,
@ -27,13 +28,14 @@ import {
TalerError, TalerError,
TalerErrorCode, TalerErrorCode,
TalerPreciseTimestamp, TalerPreciseTimestamp,
TalerUriAction,
TransactionAction, TransactionAction,
TransactionMajorState, TransactionMajorState,
TransactionMinorState, TransactionMinorState,
TransactionState, TransactionState,
TransactionType, TransactionType,
constructPayPushUri,
j2s, j2s,
stringifyTalerUri,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { import {
@ -46,6 +48,8 @@ import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { import {
PeerPushPaymentInitiationRecord, PeerPushPaymentInitiationRecord,
PeerPushPaymentInitiationStatus, PeerPushPaymentInitiationStatus,
RefreshOperationStatus,
createRefreshGroup,
} from "../index.js"; } from "../index.js";
import { PendingTaskType } from "../pending-types.js"; import { PendingTaskType } from "../pending-types.js";
import { import {
@ -64,6 +68,7 @@ import {
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { assertUnreachable } from "../util/assertUnreachable.js"; import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
const logger = new Logger("pay-peer-push-debit.ts"); const logger = new Logger("pay-peer-push-debit.ts");
@ -172,9 +177,89 @@ async function processPeerPushDebitCreateReserve(
}; };
} }
async function transitionPeerPushDebitFromReadyToDone( 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;
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, ws: InternalWalletState,
pursePub: string, pursePub: string,
transitionSpec: SimpleTransition,
): Promise<void> { ): Promise<void> {
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit, tag: TransactionType.PeerPushDebit,
@ -187,11 +272,11 @@ async function transitionPeerPushDebitFromReadyToDone(
if (!ppiRec) { if (!ppiRec) {
return undefined; return undefined;
} }
if (ppiRec.status !== PeerPushPaymentInitiationStatus.PendingReady) { if (ppiRec.status !== transitionSpec.stFrom) {
return undefined; return undefined;
} }
const oldTxState = computePeerPushDebitTransactionState(ppiRec); const oldTxState = computePeerPushDebitTransactionState(ppiRec);
ppiRec.status = PeerPushPaymentInitiationStatus.Done; ppiRec.status = transitionSpec.stTo;
const newTxState = computePeerPushDebitTransactionState(ppiRec); const newTxState = computePeerPushDebitTransactionState(ppiRec);
return { return {
oldTxState, oldTxState,
@ -201,6 +286,54 @@ async function transitionPeerPushDebitFromReadyToDone(
notifyTransition(ws, transactionId, transitionInfo); 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. * Process the "pending(ready)" state of a peer-push-debit transaction.
*/ */
@ -214,7 +347,10 @@ async function processPeerPushDebitReady(
pursePub, pursePub,
}); });
runLongpollAsync(ws, retryTag, async (ct) => { runLongpollAsync(ws, retryTag, async (ct) => {
const mergeUrl = new URL(`purses/${pursePub}/merge`); const mergeUrl = new URL(
`purses/${pursePub}/merge`,
peerPushInitiation.exchangeBaseUrl,
);
mergeUrl.searchParams.set("timeout_ms", "30000"); mergeUrl.searchParams.set("timeout_ms", "30000");
const resp = await ws.http.fetch(mergeUrl.href, { const resp = await ws.http.fetch(mergeUrl.href, {
// timeout: getReserveRequestTimeout(withdrawalGroup), // timeout: getReserveRequestTimeout(withdrawalGroup),
@ -226,16 +362,30 @@ async function processPeerPushDebitReady(
codecForExchangePurseStatus(), codecForExchangePurseStatus(),
); );
if (purseStatus.deposit_timestamp) { if (purseStatus.deposit_timestamp) {
await transitionPeerPushDebitFromReadyToDone( await transitionPeerPushDebitTransaction(
ws, ws,
peerPushInitiation.pursePub, peerPushInitiation.pursePub,
{
stFrom: PeerPushPaymentInitiationStatus.PendingReady,
stTo: PeerPushPaymentInitiationStatus.Done,
},
); );
return { return {
ready: true, ready: true,
}; };
} }
} else if (resp.status === HttpStatusCode.Gone) { } else if (resp.status === HttpStatusCode.Gone) {
// FIXME: transition the reserve into the expired state await transitionPeerPushDebitTransaction(
ws,
peerPushInitiation.pursePub,
{
stFrom: PeerPushPaymentInitiationStatus.PendingReady,
stTo: PeerPushPaymentInitiationStatus.Expired,
},
);
return {
ready: true,
};
} }
return { return {
ready: false, ready: false,
@ -280,6 +430,10 @@ export async function processPeerPushDebit(
return processPeerPushDebitCreateReserve(ws, peerPushInitiation); return processPeerPushDebitCreateReserve(ws, peerPushInitiation);
case PeerPushPaymentInitiationStatus.PendingReady: case PeerPushPaymentInitiationStatus.PendingReady:
return processPeerPushDebitReady(ws, peerPushInitiation); return processPeerPushDebitReady(ws, peerPushInitiation);
case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
return processPeerPushDebitAbortingDeletePurse(ws, peerPushInitiation);
case PeerPushPaymentInitiationStatus.AbortingRefresh:
return processPeerPushDebitAbortingRefresh(ws, peerPushInitiation);
} }
return { return {
@ -396,7 +550,8 @@ export async function initiatePeerPushDebit(
mergePriv: mergePair.priv, mergePriv: mergePair.priv,
pursePub: pursePair.pub, pursePub: pursePair.pub,
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
talerUri: constructPayPushUri({ talerUri: stringifyTalerUri({
type: TalerUriAction.PayPush,
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
contractPriv: contractKeyPair.priv, contractPriv: contractKeyPair.priv,
}), }),
@ -431,6 +586,8 @@ export function computePeerPushDebitTransactionActions(
return [TransactionAction.Suspend, TransactionAction.Abort]; return [TransactionAction.Suspend, TransactionAction.Abort];
case PeerPushPaymentInitiationStatus.Done: case PeerPushPaymentInitiationStatus.Done:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case PeerPushPaymentInitiationStatus.Expired:
return [TransactionAction.Delete];
case PeerPushPaymentInitiationStatus.Failed: case PeerPushPaymentInitiationStatus.Failed:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
} }
@ -474,9 +631,9 @@ export async function abortPeerPushDebitTransaction(
case PeerPushPaymentInitiationStatus.Done: case PeerPushPaymentInitiationStatus.Done:
case PeerPushPaymentInitiationStatus.AbortingDeletePurse: case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
case PeerPushPaymentInitiationStatus.Aborted: case PeerPushPaymentInitiationStatus.Aborted:
// Do nothing case PeerPushPaymentInitiationStatus.Expired:
break;
case PeerPushPaymentInitiationStatus.Failed: case PeerPushPaymentInitiationStatus.Failed:
// Do nothing
break; break;
default: default:
assertUnreachable(pushDebitRec.status); assertUnreachable(pushDebitRec.status);
@ -535,6 +692,7 @@ export async function failPeerPushDebitTransaction(
case PeerPushPaymentInitiationStatus.Done: case PeerPushPaymentInitiationStatus.Done:
case PeerPushPaymentInitiationStatus.Aborted: case PeerPushPaymentInitiationStatus.Aborted:
case PeerPushPaymentInitiationStatus.Failed: case PeerPushPaymentInitiationStatus.Failed:
case PeerPushPaymentInitiationStatus.Expired:
// Do nothing // Do nothing
break; break;
default: default:
@ -598,6 +756,7 @@ export async function suspendPeerPushDebitTransaction(
case PeerPushPaymentInitiationStatus.Done: case PeerPushPaymentInitiationStatus.Done:
case PeerPushPaymentInitiationStatus.Aborted: case PeerPushPaymentInitiationStatus.Aborted:
case PeerPushPaymentInitiationStatus.Failed: case PeerPushPaymentInitiationStatus.Failed:
case PeerPushPaymentInitiationStatus.Expired:
// Do nothing // Do nothing
break; break;
default: default:
@ -660,6 +819,7 @@ export async function resumePeerPushDebitTransaction(
case PeerPushPaymentInitiationStatus.Done: case PeerPushPaymentInitiationStatus.Done:
case PeerPushPaymentInitiationStatus.Aborted: case PeerPushPaymentInitiationStatus.Aborted:
case PeerPushPaymentInitiationStatus.Failed: case PeerPushPaymentInitiationStatus.Failed:
case PeerPushPaymentInitiationStatus.Expired:
// Do nothing // Do nothing
break; break;
default: default:
@ -681,7 +841,6 @@ export async function resumePeerPushDebitTransaction(
notifyTransition(ws, transactionId, transitionInfo); notifyTransition(ws, transactionId, transitionInfo);
} }
export function computePeerPushDebitTransactionState( export function computePeerPushDebitTransactionState(
ppiRecord: PeerPushPaymentInitiationRecord, ppiRecord: PeerPushPaymentInitiationRecord,
): TransactionState { ): TransactionState {
@ -738,5 +897,10 @@ export function computePeerPushDebitTransactionState(
return { return {
major: TransactionMajorState.Failed, major: TransactionMajorState.Failed,
}; };
case PeerPushPaymentInitiationStatus.Expired:
return {
major: TransactionMajorState.Expired,
};
} }
} }