wallet-core: report possible actions in transactions list
This commit is contained in:
parent
000359a5e7
commit
2a92ca8732
@ -125,6 +125,15 @@ export enum TransactionMinorState {
|
||||
AcceptRefund = "accept-refund",
|
||||
}
|
||||
|
||||
export enum TransactionAction {
|
||||
Delete = "delete",
|
||||
Suspend = "suspend",
|
||||
Resume = "resume",
|
||||
Abort = "abort",
|
||||
Fail = "fail",
|
||||
Retry = "retry",
|
||||
}
|
||||
|
||||
export interface TransactionsResponse {
|
||||
// a list of past and pending transactions sorted by pending, timestamp and transactionId.
|
||||
// In case two events are both pending and have the same timestamp,
|
||||
@ -149,6 +158,11 @@ export interface TransactionCommon {
|
||||
*/
|
||||
txState: TransactionState;
|
||||
|
||||
/**
|
||||
* Possible transitions based on the current state.
|
||||
*/
|
||||
txActions: string[];
|
||||
|
||||
/**
|
||||
* Raw amount of the transaction (exclusive of fees or other extra costs).
|
||||
*/
|
||||
|
@ -1131,7 +1131,7 @@ export enum PurchaseStatus {
|
||||
/**
|
||||
* Proposal downloaded, but the user needs to accept/reject it.
|
||||
*/
|
||||
Proposed = 30,
|
||||
DialogProposed = 30,
|
||||
|
||||
/**
|
||||
* The user has rejected the proposal.
|
||||
|
@ -418,7 +418,7 @@ export async function exportBackup(
|
||||
break;
|
||||
case PurchaseStatus.PendingPayingReplay:
|
||||
case PurchaseStatus.PendingDownloadingProposal:
|
||||
case PurchaseStatus.Proposed:
|
||||
case PurchaseStatus.DialogProposed:
|
||||
case PurchaseStatus.PendingPaying:
|
||||
propStatus = BackupProposalStatus.Proposed;
|
||||
break;
|
||||
|
@ -578,7 +578,7 @@ export async function importBackup(
|
||||
proposalStatus = PurchaseStatus.Done;
|
||||
break;
|
||||
case BackupProposalStatus.Proposed:
|
||||
proposalStatus = PurchaseStatus.Proposed;
|
||||
proposalStatus = PurchaseStatus.DialogProposed;
|
||||
break;
|
||||
case BackupProposalStatus.PermanentlyFailed:
|
||||
proposalStatus = PurchaseStatus.AbortedIncompletePayment;
|
||||
|
@ -59,11 +59,11 @@ import {
|
||||
TransactionType,
|
||||
URL,
|
||||
WireFee,
|
||||
TransactionAction,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
DenominationRecord,
|
||||
DepositGroupRecord,
|
||||
OperationStatus,
|
||||
DepositElementStatus,
|
||||
} from "../db.js";
|
||||
import { TalerError } from "@gnu-taler/taler-util";
|
||||
@ -115,6 +115,7 @@ export function computeDepositTransactionStatus(
|
||||
major: TransactionMajorState.Done,
|
||||
};
|
||||
}
|
||||
// FIXME: We should actually use separate pending states for this!
|
||||
case DepositOperationStatus.Pending: {
|
||||
const numTotal = dg.payCoinSelection.coinPubs.length;
|
||||
let numDeposited = 0;
|
||||
@ -134,9 +135,6 @@ export function computeDepositTransactionStatus(
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`num total ${numTotal}`);
|
||||
logger.info(`num deposited ${numDeposited}`);
|
||||
|
||||
if (numKycRequired > 0) {
|
||||
return {
|
||||
major: TransactionMajorState.Pending,
|
||||
@ -181,6 +179,57 @@ export function computeDepositTransactionStatus(
|
||||
}
|
||||
}
|
||||
|
||||
export function computeDepositTransactionActions(
|
||||
dg: DepositGroupRecord,
|
||||
): TransactionAction[] {
|
||||
switch (dg.operationStatus) {
|
||||
case DepositOperationStatus.Finished: {
|
||||
return [TransactionAction.Delete];
|
||||
}
|
||||
case DepositOperationStatus.Pending: {
|
||||
const numTotal = dg.payCoinSelection.coinPubs.length;
|
||||
let numDeposited = 0;
|
||||
let numKycRequired = 0;
|
||||
let numWired = 0;
|
||||
for (let i = 0; i < numTotal; i++) {
|
||||
if (dg.depositedPerCoin[i]) {
|
||||
numDeposited++;
|
||||
}
|
||||
switch (dg.transactionPerCoin[i]) {
|
||||
case DepositElementStatus.KycRequired:
|
||||
numKycRequired++;
|
||||
break;
|
||||
case DepositElementStatus.Wired:
|
||||
numWired++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (numKycRequired > 0) {
|
||||
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||
}
|
||||
|
||||
if (numDeposited == numTotal) {
|
||||
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||
}
|
||||
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
}
|
||||
case DepositOperationStatus.Suspended:
|
||||
return [TransactionAction.Resume];
|
||||
case DepositOperationStatus.Aborting:
|
||||
return [TransactionAction.Fail, TransactionAction.Suspend];
|
||||
case DepositOperationStatus.Aborted:
|
||||
return [TransactionAction.Delete];
|
||||
case DepositOperationStatus.Failed:
|
||||
return [TransactionAction.Delete];
|
||||
case DepositOperationStatus.SuspendedAborting:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
default:
|
||||
throw Error(`unexpected deposit group state (${dg.operationStatus})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function suspendDepositGroup(
|
||||
ws: InternalWalletState,
|
||||
depositGroupId: string,
|
||||
@ -309,7 +358,7 @@ export async function abortDepositGroup(
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
}
|
||||
|
||||
export async function cancelAbortingDepositGroup(
|
||||
export async function failDepositTransaction(
|
||||
ws: InternalWalletState,
|
||||
depositGroupId: string,
|
||||
): Promise<void> {
|
||||
|
@ -70,6 +70,7 @@ import {
|
||||
TalerProtocolTimestamp,
|
||||
TalerProtocolViolationError,
|
||||
TalerUriAction,
|
||||
TransactionAction,
|
||||
TransactionMajorState,
|
||||
TransactionMinorState,
|
||||
TransactionState,
|
||||
@ -547,7 +548,7 @@ async function processDownloadProposal(
|
||||
await tx.purchases.put(p);
|
||||
}
|
||||
} else {
|
||||
p.purchaseStatus = PurchaseStatus.Proposed;
|
||||
p.purchaseStatus = PurchaseStatus.DialogProposed;
|
||||
await tx.purchases.put(p);
|
||||
}
|
||||
const newTxState = computePayMerchantTransactionState(p);
|
||||
@ -994,7 +995,7 @@ export async function checkPaymentByProposalId(
|
||||
return tx.purchases.get(proposalId);
|
||||
});
|
||||
|
||||
if (!purchase || purchase.purchaseStatus === PurchaseStatus.Proposed) {
|
||||
if (!purchase || purchase.purchaseStatus === PurchaseStatus.DialogProposed) {
|
||||
// If not already paid, check if we could pay for it.
|
||||
const res = await selectPayCoinsNew(ws, {
|
||||
auditors: [],
|
||||
@ -1417,7 +1418,7 @@ export async function confirmPay(
|
||||
}
|
||||
const oldTxState = computePayMerchantTransactionState(p);
|
||||
switch (p.purchaseStatus) {
|
||||
case PurchaseStatus.Proposed:
|
||||
case PurchaseStatus.DialogProposed:
|
||||
p.payInfo = {
|
||||
payCoinSelection: coinSelection,
|
||||
payCoinSelectionUid: encodeCrock(getRandomBytes(16)),
|
||||
@ -1498,7 +1499,7 @@ export async function processPurchase(
|
||||
case PurchaseStatus.FailedClaim:
|
||||
case PurchaseStatus.Done:
|
||||
case PurchaseStatus.RepurchaseDetected:
|
||||
case PurchaseStatus.Proposed:
|
||||
case PurchaseStatus.DialogProposed:
|
||||
case PurchaseStatus.AbortedProposalRefused:
|
||||
case PurchaseStatus.AbortedIncompletePayment:
|
||||
case PurchaseStatus.SuspendedAbortingWithRefund:
|
||||
@ -1713,7 +1714,7 @@ export async function refuseProposal(
|
||||
logger.trace(`proposal ${proposalId} not found, won't refuse proposal`);
|
||||
return undefined;
|
||||
}
|
||||
if (proposal.purchaseStatus !== PurchaseStatus.Proposed) {
|
||||
if (proposal.purchaseStatus !== PurchaseStatus.DialogProposed) {
|
||||
return undefined;
|
||||
}
|
||||
const oldTxState = computePayMerchantTransactionState(proposal);
|
||||
@ -1791,7 +1792,7 @@ export async function abortPayMerchant(
|
||||
ws.workAvailable.trigger();
|
||||
}
|
||||
|
||||
export async function cancelAbortingPaymentTransaction(
|
||||
export async function failPaymentTransaction(
|
||||
ws: InternalWalletState,
|
||||
proposalId: string,
|
||||
): Promise<void> {
|
||||
@ -2023,7 +2024,7 @@ export function computePayMerchantTransactionState(
|
||||
major: TransactionMajorState.SuspendedAborting,
|
||||
};
|
||||
// Dialog States
|
||||
case PurchaseStatus.Proposed:
|
||||
case PurchaseStatus.DialogProposed:
|
||||
return {
|
||||
major: TransactionMajorState.Dialog,
|
||||
minor: TransactionMinorState.MerchantOrderProposed,
|
||||
@ -2060,6 +2061,68 @@ export function computePayMerchantTransactionState(
|
||||
}
|
||||
}
|
||||
|
||||
export function computePayMerchantTransactionActions(
|
||||
purchaseRecord: PurchaseRecord,
|
||||
): TransactionAction[] {
|
||||
switch (purchaseRecord.purchaseStatus) {
|
||||
// Pending States
|
||||
case PurchaseStatus.PendingDownloadingProposal:
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
case PurchaseStatus.PendingPaying:
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
case PurchaseStatus.PendingPayingReplay:
|
||||
// Special "abort" since it goes back to "done".
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
case PurchaseStatus.PendingQueryingAutoRefund:
|
||||
// Special "abort" since it goes back to "done".
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
case PurchaseStatus.PendingQueryingRefund:
|
||||
// Special "abort" since it goes back to "done".
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
case PurchaseStatus.PendingAcceptRefund:
|
||||
// Special "abort" since it goes back to "done".
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
// Suspended Pending States
|
||||
case PurchaseStatus.SuspendedDownloadingProposal:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case PurchaseStatus.SuspendedPaying:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case PurchaseStatus.SuspendedPayingReplay:
|
||||
// Special "abort" since it goes back to "done".
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case PurchaseStatus.SuspendedQueryingAutoRefund:
|
||||
// Special "abort" since it goes back to "done".
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case PurchaseStatus.SuspendedQueryingRefund:
|
||||
// Special "abort" since it goes back to "done".
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case PurchaseStatus.SuspendedPendingAcceptRefund:
|
||||
// Special "abort" since it goes back to "done".
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
// Aborting States
|
||||
case PurchaseStatus.AbortingWithRefund:
|
||||
return [TransactionAction.Fail, TransactionAction.Suspend];
|
||||
case PurchaseStatus.SuspendedAbortingWithRefund:
|
||||
return [TransactionAction.Fail, TransactionAction.Resume];
|
||||
// Dialog States
|
||||
case PurchaseStatus.DialogProposed:
|
||||
return [];
|
||||
// Final States
|
||||
case PurchaseStatus.AbortedProposalRefused:
|
||||
return [TransactionAction.Delete];
|
||||
case PurchaseStatus.Done:
|
||||
return [TransactionAction.Delete];
|
||||
case PurchaseStatus.RepurchaseDetected:
|
||||
return [TransactionAction.Delete];
|
||||
case PurchaseStatus.AbortedIncompletePayment:
|
||||
return [TransactionAction.Delete];
|
||||
case PurchaseStatus.FailedClaim:
|
||||
return [TransactionAction.Delete];
|
||||
case PurchaseStatus.FailedAbort:
|
||||
return [TransactionAction.Delete];
|
||||
}
|
||||
}
|
||||
|
||||
async function processPurchaseAutoRefund(
|
||||
ws: InternalWalletState,
|
||||
purchase: PurchaseRecord,
|
||||
|
@ -79,6 +79,7 @@ import {
|
||||
TransactionMajorState,
|
||||
TransactionMinorState,
|
||||
TalerPreciseTimestamp,
|
||||
TransactionAction,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { SpendCoinDetails } from "../crypto/cryptoImplementation.js";
|
||||
import {
|
||||
@ -2008,7 +2009,36 @@ export function computePeerPushDebitTransactionState(
|
||||
case PeerPushPaymentInitiationStatus.Failed:
|
||||
return {
|
||||
major: TransactionMajorState.Failed,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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.Failed:
|
||||
return [TransactionAction.Delete];
|
||||
}
|
||||
}
|
||||
|
||||
@ -2072,7 +2102,7 @@ export async function abortPeerPushDebitTransaction(
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
}
|
||||
|
||||
export async function cancelAbortingPeerPushDebitTransaction(
|
||||
export async function failPeerPushDebitTransaction(
|
||||
ws: InternalWalletState,
|
||||
pursePub: string,
|
||||
) {
|
||||
@ -2316,8 +2346,7 @@ export async function abortPeerPullDebitTransaction(
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
}
|
||||
|
||||
|
||||
export async function cancelAbortingPeerPullDebitTransaction(
|
||||
export async function failPeerPullDebitTransaction(
|
||||
ws: InternalWalletState,
|
||||
peerPullPaymentIncomingId: string,
|
||||
) {
|
||||
@ -2501,7 +2530,6 @@ export async function suspendPeerPushCreditTransaction(
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
}
|
||||
|
||||
|
||||
export async function abortPeerPushCreditTransaction(
|
||||
ws: InternalWalletState,
|
||||
peerPushPaymentIncomingId: string,
|
||||
@ -2568,7 +2596,7 @@ export async function abortPeerPushCreditTransaction(
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
}
|
||||
|
||||
export async function cancelAbortingPeerPushCreditTransaction(
|
||||
export async function failPeerPushCreditTransaction(
|
||||
ws: InternalWalletState,
|
||||
peerPushPaymentIncomingId: string,
|
||||
) {
|
||||
@ -2765,7 +2793,7 @@ export async function abortPeerPullCreditTransaction(
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
}
|
||||
|
||||
export async function cancelAbortingPeerPullCreditTransaction(
|
||||
export async function failPeerPullCreditTransaction(
|
||||
ws: InternalWalletState,
|
||||
pursePub: string,
|
||||
) {
|
||||
@ -2996,12 +3024,41 @@ export function computePeerPushCreditTransactionState(
|
||||
};
|
||||
case PeerPushPaymentIncomingStatus.Aborted:
|
||||
return {
|
||||
major: TransactionMajorState.Aborted
|
||||
major: TransactionMajorState.Aborted,
|
||||
};
|
||||
case PeerPushPaymentIncomingStatus.Failed:
|
||||
return {
|
||||
major: TransactionMajorState.Failed,
|
||||
}
|
||||
};
|
||||
default:
|
||||
assertUnreachable(pushCreditRecord.status);
|
||||
}
|
||||
}
|
||||
|
||||
export function computePeerPushCreditTransactionActions(
|
||||
pushCreditRecord: PeerPushPaymentIncomingRecord,
|
||||
): TransactionAction[] {
|
||||
switch (pushCreditRecord.status) {
|
||||
case PeerPushPaymentIncomingStatus.DialogProposed:
|
||||
return [];
|
||||
case PeerPushPaymentIncomingStatus.PendingMerge:
|
||||
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||
case PeerPushPaymentIncomingStatus.Done:
|
||||
return [TransactionAction.Delete];
|
||||
case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
|
||||
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||
case PeerPushPaymentIncomingStatus.PendingWithdrawing:
|
||||
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||
case PeerPushPaymentIncomingStatus.SuspendedMerge:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
case PeerPushPaymentIncomingStatus.Aborted:
|
||||
return [TransactionAction.Delete];
|
||||
case PeerPushPaymentIncomingStatus.Failed:
|
||||
return [TransactionAction.Delete];
|
||||
default:
|
||||
assertUnreachable(pushCreditRecord.status);
|
||||
}
|
||||
@ -3076,6 +3133,39 @@ export function computePeerPullCreditTransactionState(
|
||||
}
|
||||
}
|
||||
|
||||
export function computePeerPullCreditTransactionActions(
|
||||
pullCreditRecord: PeerPullPaymentInitiationRecord,
|
||||
): TransactionAction[] {
|
||||
switch (pullCreditRecord.status) {
|
||||
case PeerPullPaymentInitiationStatus.PendingCreatePurse:
|
||||
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||
case PeerPullPaymentInitiationStatus.PendingMergeKycRequired:
|
||||
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||
case PeerPullPaymentInitiationStatus.PendingReady:
|
||||
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||
case PeerPullPaymentInitiationStatus.DonePurseDeposited:
|
||||
return [TransactionAction.Delete];
|
||||
case PeerPullPaymentInitiationStatus.PendingWithdrawing:
|
||||
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||
case PeerPullPaymentInitiationStatus.SuspendedCreatePurse:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case PeerPullPaymentInitiationStatus.SuspendedReady:
|
||||
return [TransactionAction.Abort, TransactionAction.Resume];
|
||||
case PeerPullPaymentInitiationStatus.SuspendedWithdrawing:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
case PeerPullPaymentInitiationStatus.Aborted:
|
||||
return [TransactionAction.Delete];
|
||||
case PeerPullPaymentInitiationStatus.AbortingDeletePurse:
|
||||
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||
case PeerPullPaymentInitiationStatus.Failed:
|
||||
return [TransactionAction.Delete];
|
||||
case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
}
|
||||
}
|
||||
|
||||
export function computePeerPullDebitTransactionState(
|
||||
pullDebitRecord: PeerPullPaymentIncomingRecord,
|
||||
): TransactionState {
|
||||
@ -3119,3 +3209,26 @@ export function computePeerPullDebitTransactionState(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function computePeerPullDebitTransactionActions(
|
||||
pullDebitRecord: PeerPullPaymentIncomingRecord,
|
||||
): TransactionAction[] {
|
||||
switch (pullDebitRecord.status) {
|
||||
case PeerPullDebitRecordStatus.DialogProposed:
|
||||
return [];
|
||||
case PeerPullDebitRecordStatus.PendingDeposit:
|
||||
return [TransactionAction.Abort, TransactionAction.Suspend];
|
||||
case PeerPullDebitRecordStatus.DonePaid:
|
||||
return [TransactionAction.Delete];
|
||||
case PeerPullDebitRecordStatus.SuspendedDeposit:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case PeerPullDebitRecordStatus.Aborted:
|
||||
return [TransactionAction.Delete];
|
||||
case PeerPullDebitRecordStatus.AbortingRefresh:
|
||||
return [TransactionAction.Fail, TransactionAction.Suspend];
|
||||
case PeerPullDebitRecordStatus.Failed:
|
||||
return [TransactionAction.Delete];
|
||||
case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ import {
|
||||
TalerErrorDetail,
|
||||
TalerPreciseTimestamp,
|
||||
TalerProtocolTimestamp,
|
||||
TransactionAction,
|
||||
TransactionMajorState,
|
||||
TransactionState,
|
||||
TransactionType,
|
||||
@ -96,7 +97,10 @@ import {
|
||||
PendingTaskType,
|
||||
WalletConfig,
|
||||
} from "../index.js";
|
||||
import { constructTransactionIdentifier, notifyTransition } from "./transactions.js";
|
||||
import {
|
||||
constructTransactionIdentifier,
|
||||
notifyTransition,
|
||||
} from "./transactions.js";
|
||||
|
||||
const logger = new Logger("refresh.ts");
|
||||
|
||||
@ -1076,8 +1080,12 @@ export async function createRefreshGroup(
|
||||
* Timestamp after which the wallet would do the next check for an auto-refresh.
|
||||
*/
|
||||
function getAutoRefreshCheckThreshold(d: DenominationRecord): AbsoluteTime {
|
||||
const expireWithdraw = AbsoluteTime.fromProtocolTimestamp(d.stampExpireWithdraw);
|
||||
const expireDeposit = AbsoluteTime.fromProtocolTimestamp(d.stampExpireDeposit);
|
||||
const expireWithdraw = AbsoluteTime.fromProtocolTimestamp(
|
||||
d.stampExpireWithdraw,
|
||||
);
|
||||
const expireDeposit = AbsoluteTime.fromProtocolTimestamp(
|
||||
d.stampExpireDeposit,
|
||||
);
|
||||
const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit);
|
||||
const deltaDiv = durationMul(delta, 0.75);
|
||||
return AbsoluteTime.addDuration(expireWithdraw, deltaDiv);
|
||||
@ -1087,8 +1095,12 @@ function getAutoRefreshCheckThreshold(d: DenominationRecord): AbsoluteTime {
|
||||
* Timestamp after which the wallet would do an auto-refresh.
|
||||
*/
|
||||
function getAutoRefreshExecuteThreshold(d: DenominationRecord): AbsoluteTime {
|
||||
const expireWithdraw = AbsoluteTime.fromProtocolTimestamp(d.stampExpireWithdraw);
|
||||
const expireDeposit = AbsoluteTime.fromProtocolTimestamp(d.stampExpireDeposit);
|
||||
const expireWithdraw = AbsoluteTime.fromProtocolTimestamp(
|
||||
d.stampExpireWithdraw,
|
||||
);
|
||||
const expireDeposit = AbsoluteTime.fromProtocolTimestamp(
|
||||
d.stampExpireDeposit,
|
||||
);
|
||||
const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit);
|
||||
const deltaDiv = durationMul(delta, 0.5);
|
||||
return AbsoluteTime.addDuration(expireWithdraw, deltaDiv);
|
||||
@ -1175,7 +1187,8 @@ export async function autoRefresh(
|
||||
logger.info(
|
||||
`next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,
|
||||
);
|
||||
exchange.nextRefreshCheck = AbsoluteTime.toPreciseTimestamp(minCheckThreshold);
|
||||
exchange.nextRefreshCheck =
|
||||
AbsoluteTime.toPreciseTimestamp(minCheckThreshold);
|
||||
await tx.exchanges.put(exchange);
|
||||
});
|
||||
return OperationAttemptResult.finishedEmpty();
|
||||
@ -1204,6 +1217,21 @@ export function computeRefreshTransactionState(
|
||||
}
|
||||
}
|
||||
|
||||
export function computeRefreshTransactionActions(
|
||||
rg: RefreshGroupRecord,
|
||||
): TransactionAction[] {
|
||||
switch (rg.operationStatus) {
|
||||
case RefreshOperationStatus.Finished:
|
||||
return [TransactionAction.Delete];
|
||||
case RefreshOperationStatus.Failed:
|
||||
return [TransactionAction.Delete];
|
||||
case RefreshOperationStatus.Pending:
|
||||
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||
case RefreshOperationStatus.Suspended:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
}
|
||||
}
|
||||
|
||||
export async function suspendRefreshGroup(
|
||||
ws: InternalWalletState,
|
||||
refreshGroupId: string,
|
||||
@ -1292,7 +1320,7 @@ export async function resumeRefreshGroup(
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
}
|
||||
|
||||
export async function cancelAbortingRefreshGroup(
|
||||
export async function failRefreshGroup(
|
||||
ws: InternalWalletState,
|
||||
refreshGroupId: string,
|
||||
): Promise<void> {
|
||||
@ -1321,7 +1349,7 @@ export async function abortRefreshGroup(
|
||||
let newStatus: RefreshOperationStatus | undefined;
|
||||
switch (dg.operationStatus) {
|
||||
case RefreshOperationStatus.Finished:
|
||||
break;;
|
||||
break;
|
||||
case RefreshOperationStatus.Pending:
|
||||
case RefreshOperationStatus.Suspended:
|
||||
newStatus = RefreshOperationStatus.Failed;
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
TalerPreciseTimestamp,
|
||||
TalerProtocolTimestamp,
|
||||
TipPlanchetDetail,
|
||||
TransactionAction,
|
||||
TransactionMajorState,
|
||||
TransactionMinorState,
|
||||
TransactionState,
|
||||
@ -111,6 +112,24 @@ export function computeTipTransactionStatus(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function computeTipTransactionActions(
|
||||
tipRecord: TipRecord,
|
||||
): TransactionAction[] {
|
||||
switch (tipRecord.status) {
|
||||
case TipRecordStatus.Done:
|
||||
return [TransactionAction.Delete];
|
||||
case TipRecordStatus.Aborted:
|
||||
return [TransactionAction.Delete];
|
||||
case TipRecordStatus.PendingPickup:
|
||||
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||
case TipRecordStatus.SuspendidPickup:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
default:
|
||||
assertUnreachable(tipRecord.status);
|
||||
}
|
||||
}
|
||||
|
||||
export async function prepareTip(
|
||||
ws: InternalWalletState,
|
||||
talerTipUri: string,
|
||||
@ -526,7 +545,7 @@ export async function resumeTipTransaction(
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
}
|
||||
|
||||
export async function cancelAbortingTipTransaction(
|
||||
export async function failTipTransaction(
|
||||
ws: InternalWalletState,
|
||||
walletTipId: string,
|
||||
): Promise<void> {
|
||||
|
@ -73,32 +73,34 @@ import { constructTaskIdentifier, TaskIdentifiers } from "../util/retries.js";
|
||||
import { resetOperationTimeout, TombstoneTag } from "./common.js";
|
||||
import {
|
||||
abortDepositGroup,
|
||||
cancelAbortingDepositGroup,
|
||||
failDepositTransaction,
|
||||
computeDepositTransactionStatus,
|
||||
deleteDepositGroup,
|
||||
resumeDepositGroup,
|
||||
suspendDepositGroup,
|
||||
computeDepositTransactionActions,
|
||||
} from "./deposits.js";
|
||||
import { getExchangeDetails } from "./exchanges.js";
|
||||
import {
|
||||
abortPayMerchant,
|
||||
cancelAbortingPaymentTransaction,
|
||||
failPaymentTransaction,
|
||||
computePayMerchantTransactionState,
|
||||
computeRefundTransactionState,
|
||||
expectProposalDownload,
|
||||
extractContractData,
|
||||
resumePayMerchant,
|
||||
suspendPayMerchant,
|
||||
computePayMerchantTransactionActions,
|
||||
} from "./pay-merchant.js";
|
||||
import {
|
||||
abortPeerPullCreditTransaction,
|
||||
abortPeerPullDebitTransaction,
|
||||
abortPeerPushCreditTransaction,
|
||||
abortPeerPushDebitTransaction,
|
||||
cancelAbortingPeerPullCreditTransaction,
|
||||
cancelAbortingPeerPullDebitTransaction,
|
||||
cancelAbortingPeerPushCreditTransaction,
|
||||
cancelAbortingPeerPushDebitTransaction,
|
||||
failPeerPullCreditTransaction,
|
||||
failPeerPullDebitTransaction,
|
||||
failPeerPushCreditTransaction,
|
||||
failPeerPushDebitTransaction,
|
||||
computePeerPullCreditTransactionState,
|
||||
computePeerPullDebitTransactionState,
|
||||
computePeerPushCreditTransactionState,
|
||||
@ -111,28 +113,35 @@ import {
|
||||
suspendPeerPullDebitTransaction,
|
||||
suspendPeerPushCreditTransaction,
|
||||
suspendPeerPushDebitTransaction,
|
||||
computePeerPushDebitTransactionActions,
|
||||
computePeerPullDebitTransactionActions,
|
||||
computePeerPullCreditTransactionActions,
|
||||
computePeerPushCreditTransactionActions,
|
||||
} from "./pay-peer.js";
|
||||
import {
|
||||
abortRefreshGroup,
|
||||
cancelAbortingRefreshGroup,
|
||||
failRefreshGroup,
|
||||
computeRefreshTransactionState,
|
||||
resumeRefreshGroup,
|
||||
suspendRefreshGroup,
|
||||
computeRefreshTransactionActions,
|
||||
} from "./refresh.js";
|
||||
import {
|
||||
abortTipTransaction,
|
||||
cancelAbortingTipTransaction,
|
||||
failTipTransaction,
|
||||
computeTipTransactionStatus,
|
||||
resumeTipTransaction,
|
||||
suspendTipTransaction,
|
||||
computeTipTransactionActions,
|
||||
} from "./tip.js";
|
||||
import {
|
||||
abortWithdrawalTransaction,
|
||||
augmentPaytoUrisForWithdrawal,
|
||||
cancelAbortingWithdrawalTransaction,
|
||||
failWithdrawalTransaction,
|
||||
computeWithdrawalTransactionStatus,
|
||||
resumeWithdrawalTransaction,
|
||||
suspendWithdrawalTransaction,
|
||||
computeWithdrawalTransactionActions,
|
||||
} from "./withdraw.js";
|
||||
|
||||
const logger = new Logger("taler-wallet-core:transactions.ts");
|
||||
@ -429,6 +438,7 @@ function buildTransactionForPushPaymentDebit(
|
||||
return {
|
||||
type: TransactionType.PeerPushDebit,
|
||||
txState: computePeerPushDebitTransactionState(pi),
|
||||
txActions: computePeerPushDebitTransactionActions(pi),
|
||||
amountEffective: pi.totalCost,
|
||||
amountRaw: pi.amount,
|
||||
exchangeBaseUrl: pi.exchangeBaseUrl,
|
||||
@ -456,6 +466,7 @@ function buildTransactionForPullPaymentDebit(
|
||||
return {
|
||||
type: TransactionType.PeerPullDebit,
|
||||
txState: computePeerPullDebitTransactionState(pi),
|
||||
txActions: computePeerPullDebitTransactionActions(pi),
|
||||
amountEffective: pi.coinSel?.totalCost
|
||||
? pi.coinSel?.totalCost
|
||||
: Amounts.stringify(pi.contractTerms.amount),
|
||||
@ -503,6 +514,7 @@ function buildTransactionForPeerPullCredit(
|
||||
return {
|
||||
type: TransactionType.PeerPullCredit,
|
||||
txState: computePeerPullCreditTransactionState(pullCredit),
|
||||
txActions: computePeerPullCreditTransactionActions(pullCredit),
|
||||
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
||||
amountRaw: Amounts.stringify(wsr.instructedAmount),
|
||||
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
||||
@ -533,6 +545,7 @@ function buildTransactionForPeerPullCredit(
|
||||
return {
|
||||
type: TransactionType.PeerPullCredit,
|
||||
txState: computePeerPullCreditTransactionState(pullCredit),
|
||||
txActions: computePeerPullCreditTransactionActions(pullCredit),
|
||||
amountEffective: Amounts.stringify(pullCredit.estimatedAmountEffective),
|
||||
amountRaw: Amounts.stringify(peerContractTerms.amount),
|
||||
exchangeBaseUrl: pullCredit.exchangeBaseUrl,
|
||||
@ -569,6 +582,7 @@ function buildTransactionForPeerPushCredit(
|
||||
return {
|
||||
type: TransactionType.PeerPushCredit,
|
||||
txState: computePeerPushCreditTransactionState(pushInc),
|
||||
txActions: computePeerPushCreditTransactionActions(pushInc),
|
||||
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
||||
amountRaw: Amounts.stringify(wsr.instructedAmount),
|
||||
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
||||
@ -588,6 +602,7 @@ function buildTransactionForPeerPushCredit(
|
||||
return {
|
||||
type: TransactionType.PeerPushCredit,
|
||||
txState: computePeerPushCreditTransactionState(pushInc),
|
||||
txActions: computePeerPushCreditTransactionActions(pushInc),
|
||||
// FIXME: This is wrong, needs to consider fees!
|
||||
amountEffective: Amounts.stringify(peerContractTerms.amount),
|
||||
amountRaw: Amounts.stringify(peerContractTerms.amount),
|
||||
@ -615,6 +630,7 @@ function buildTransactionForBankIntegratedWithdraw(
|
||||
return {
|
||||
type: TransactionType.Withdrawal,
|
||||
txState: computeWithdrawalTransactionStatus(wgRecord),
|
||||
txActions: computeWithdrawalTransactionActions(wgRecord),
|
||||
amountEffective: Amounts.stringify(wgRecord.denomsSel.totalCoinValue),
|
||||
amountRaw: Amounts.stringify(wgRecord.instructedAmount),
|
||||
withdrawalDetails: {
|
||||
@ -656,6 +672,7 @@ function buildTransactionForManualWithdraw(
|
||||
return {
|
||||
type: TransactionType.Withdrawal,
|
||||
txState: computeWithdrawalTransactionStatus(withdrawalGroup),
|
||||
txActions: computeWithdrawalTransactionActions(withdrawalGroup),
|
||||
amountEffective: Amounts.stringify(
|
||||
withdrawalGroup.denomsSel.totalCoinValue,
|
||||
),
|
||||
@ -706,6 +723,7 @@ function buildTransactionForRefund(
|
||||
refundGroupId: refundRecord.refundGroupId,
|
||||
}),
|
||||
txState: computeRefundTransactionState(refundRecord),
|
||||
txActions: [],
|
||||
paymentInfo,
|
||||
};
|
||||
}
|
||||
@ -725,6 +743,7 @@ function buildTransactionForRefresh(
|
||||
return {
|
||||
type: TransactionType.Refresh,
|
||||
txState: computeRefreshTransactionState(refreshGroupRecord),
|
||||
txActions: computeRefreshTransactionActions(refreshGroupRecord),
|
||||
refreshReason: refreshGroupRecord.reason,
|
||||
amountEffective: Amounts.stringify(
|
||||
Amounts.zeroOfCurrency(refreshGroupRecord.currency),
|
||||
@ -759,6 +778,7 @@ function buildTransactionForDeposit(
|
||||
return {
|
||||
type: TransactionType.Deposit,
|
||||
txState: computeDepositTransactionStatus(dg),
|
||||
txActions: computeDepositTransactionActions(dg),
|
||||
amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
|
||||
amountEffective: Amounts.stringify(dg.totalPayCost),
|
||||
timestamp: dg.timestampCreated,
|
||||
@ -791,6 +811,7 @@ function buildTransactionForTip(
|
||||
return {
|
||||
type: TransactionType.Tip,
|
||||
txState: computeTipTransactionStatus(tipRecord),
|
||||
txActions: computeTipTransactionActions(tipRecord),
|
||||
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
|
||||
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
|
||||
timestamp: tipRecord.acceptedTimestamp,
|
||||
@ -860,6 +881,7 @@ async function buildTransactionForPurchase(
|
||||
return {
|
||||
type: TransactionType.Payment,
|
||||
txState: computePayMerchantTransactionState(purchaseRecord),
|
||||
txActions: computePayMerchantTransactionActions(purchaseRecord),
|
||||
amountRaw: Amounts.stringify(contractData.amount),
|
||||
amountEffective: Amounts.stringify(purchaseRecord.payInfo.totalPayCost),
|
||||
totalRefundRaw: Amounts.stringify(zero), // FIXME!
|
||||
@ -1493,7 +1515,7 @@ export async function suspendTransaction(
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelAbortingTransaction(
|
||||
export async function failTransaction(
|
||||
ws: InternalWalletState,
|
||||
transactionId: string,
|
||||
): Promise<void> {
|
||||
@ -1503,34 +1525,34 @@ export async function cancelAbortingTransaction(
|
||||
}
|
||||
switch (tx.tag) {
|
||||
case TransactionType.Deposit:
|
||||
await cancelAbortingDepositGroup(ws, tx.depositGroupId);
|
||||
await failDepositTransaction(ws, tx.depositGroupId);
|
||||
return;
|
||||
case TransactionType.InternalWithdrawal:
|
||||
case TransactionType.Withdrawal:
|
||||
await cancelAbortingWithdrawalTransaction(ws, tx.withdrawalGroupId);
|
||||
await failWithdrawalTransaction(ws, tx.withdrawalGroupId);
|
||||
return;
|
||||
case TransactionType.Payment:
|
||||
await cancelAbortingPaymentTransaction(ws, tx.proposalId);
|
||||
await failPaymentTransaction(ws, tx.proposalId);
|
||||
return;
|
||||
case TransactionType.Refund:
|
||||
throw Error("can't do cancel-aborting on refund transaction");
|
||||
case TransactionType.Tip:
|
||||
await cancelAbortingTipTransaction(ws, tx.walletTipId);
|
||||
await failTipTransaction(ws, tx.walletTipId);
|
||||
return;
|
||||
case TransactionType.Refresh:
|
||||
await cancelAbortingRefreshGroup(ws, tx.refreshGroupId);
|
||||
await failRefreshGroup(ws, tx.refreshGroupId);
|
||||
return;
|
||||
case TransactionType.PeerPullCredit:
|
||||
await cancelAbortingPeerPullCreditTransaction(ws, tx.pursePub);
|
||||
await failPeerPullCreditTransaction(ws, tx.pursePub);
|
||||
return;
|
||||
case TransactionType.PeerPullDebit:
|
||||
await cancelAbortingPeerPullDebitTransaction(ws, tx.peerPullPaymentIncomingId);
|
||||
await failPeerPullDebitTransaction(ws, tx.peerPullPaymentIncomingId);
|
||||
return;
|
||||
case TransactionType.PeerPushCredit:
|
||||
await cancelAbortingPeerPushCreditTransaction(ws, tx.peerPushPaymentIncomingId);
|
||||
await failPeerPushCreditTransaction(ws, tx.peerPushPaymentIncomingId);
|
||||
return;
|
||||
case TransactionType.PeerPushDebit:
|
||||
await cancelAbortingPeerPushDebitTransaction(ws, tx.pursePub);
|
||||
await failPeerPushDebitTransaction(ws, tx.pursePub);
|
||||
return;
|
||||
default:
|
||||
assertUnreachable(tx);
|
||||
|
@ -67,6 +67,7 @@ import {
|
||||
TransactionMajorState,
|
||||
TransactionMinorState,
|
||||
TalerPreciseTimestamp,
|
||||
TransactionAction,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
||||
import {
|
||||
@ -338,7 +339,7 @@ export async function abortWithdrawalTransaction(
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
}
|
||||
|
||||
export async function cancelAbortingWithdrawalTransaction(
|
||||
export async function failWithdrawalTransaction(
|
||||
ws: InternalWalletState,
|
||||
withdrawalGroupId: string,
|
||||
) {
|
||||
@ -483,6 +484,49 @@ export function computeWithdrawalTransactionStatus(
|
||||
}
|
||||
}
|
||||
|
||||
export function computeWithdrawalTransactionActions(
|
||||
wgRecord: WithdrawalGroupRecord,
|
||||
): TransactionAction[] {
|
||||
switch (wgRecord.status) {
|
||||
case WithdrawalGroupStatus.FailedBankAborted:
|
||||
return [TransactionAction.Delete];
|
||||
case WithdrawalGroupStatus.Finished:
|
||||
return [TransactionAction.Delete];
|
||||
case WithdrawalGroupStatus.PendingRegisteringBank:
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.PendingReady:
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.PendingQueryingStatus:
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.PendingWaitConfirmBank:
|
||||
return [TransactionAction.Suspend, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.AbortingBank:
|
||||
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||
case WithdrawalGroupStatus.SuspendedAbortingBank:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
case WithdrawalGroupStatus.SuspendedQueryingStatus:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.SuspendedRegisteringBank:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort]
|
||||
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.SuspendedReady:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.PendingAml:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.PendingKyc:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.SuspendedAml:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort];
|
||||
case WithdrawalGroupStatus.SuspendedKyc:
|
||||
return [TransactionAction.Resume, TransactionAction.Abort]
|
||||
case WithdrawalGroupStatus.FailedAbortingBank:
|
||||
return [TransactionAction.Delete];
|
||||
case WithdrawalGroupStatus.AbortedExchange:
|
||||
return [TransactionAction.Delete];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about a withdrawal from
|
||||
* a taler://withdraw URI by asking the bank.
|
||||
|
@ -232,7 +232,7 @@ import {
|
||||
import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
|
||||
import {
|
||||
abortTransaction,
|
||||
cancelAbortingTransaction,
|
||||
failTransaction,
|
||||
deleteTransaction,
|
||||
getTransactionById,
|
||||
getTransactions,
|
||||
@ -1233,7 +1233,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
}
|
||||
case WalletApiOperation.CancelAbortingTransaction: {
|
||||
const req = codecForCancelAbortingTransactionRequest().decode(payload);
|
||||
await cancelAbortingTransaction(ws, req.transactionId);
|
||||
await failTransaction(ws, req.transactionId);
|
||||
return {};
|
||||
}
|
||||
case WalletApiOperation.ResumeTransaction: {
|
||||
|
Loading…
Reference in New Issue
Block a user