wallet-core: handle more p2p abort cases nicely
This commit is contained in:
parent
6e7c88a620
commit
9fca44893a
@ -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 };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user