summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-05-02 10:59:50 +0200
committerFlorian Dold <florian@dold.me>2023-05-02 10:59:50 +0200
commit16d30adf0d57f6d954230c437e56e8a8700ef2ae (patch)
tree1f70267b3d4abd68df5be7da8ac769025a847e4e /packages/taler-wallet-core/src
parentc4f5c83b8e8614ead5f48952ea8b60b5b3a3971c (diff)
downloadwallet-core-16d30adf0d57f6d954230c437e56e8a8700ef2ae.tar.gz
wallet-core-16d30adf0d57f6d954230c437e56e8a8700ef2ae.tar.bz2
wallet-core-16d30adf0d57f6d954230c437e56e8a8700ef2ae.zip
-withdrawal notifications
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts48
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts205
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts14
-rw-r--r--packages/taler-wallet-core/src/wallet.ts2
4 files changed, 187 insertions, 82 deletions
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index cacc179f2..02b0b56ba 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -26,6 +26,7 @@ import {
ExtendedStatus,
j2s,
Logger,
+ NotificationType,
OrderShortInfo,
PaymentStatus,
PeerContractTerms,
@@ -38,6 +39,7 @@ import {
TransactionMajorState,
TransactionsRequest,
TransactionsResponse,
+ TransactionState,
TransactionType,
WithdrawalType,
} from "@gnu-taler/taler-util";
@@ -94,6 +96,7 @@ import { processPeerPullCredit } from "./pay-peer.js";
import { processRefreshGroup } from "./refresh.js";
import { computeTipTransactionStatus, processTip } from "./tip.js";
import {
+ abortWithdrawalTransaction,
augmentPaytoUrisForWithdrawal,
computeWithdrawalTransactionStatus,
processWithdrawalGroup,
@@ -1854,24 +1857,55 @@ export async function deleteTransaction(
export async function abortTransaction(
ws: InternalWalletState,
transactionId: string,
- forceImmediateAbort?: boolean,
): Promise<void> {
- const { type, args: rest } = parseId("txn", transactionId);
+ const txId = parseTransactionIdentifier(transactionId);
+ if (!txId) {
+ throw Error("invalid transaction identifier");
+ }
- switch (type) {
+ switch (txId.tag) {
case TransactionType.Payment: {
- const proposalId = rest[0];
- await abortPay(ws, proposalId, forceImmediateAbort);
+ await abortPay(ws, txId.proposalId);
break;
}
- case TransactionType.PeerPushDebit: {
+ case TransactionType.Withdrawal: {
+ await abortWithdrawalTransaction(ws, txId.withdrawalGroupId);
break;
}
default: {
- const unknownTxType: any = type;
+ const unknownTxType: any = txId.tag;
throw Error(
`can't abort a '${unknownTxType}' transaction: not yet implemented`,
);
}
}
}
+
+export interface TransitionInfo {
+ oldTxState: TransactionState;
+ newTxState: TransactionState;
+}
+
+/**
+ * Notify of a state transition if necessary.
+ */
+export function notifyTransition(
+ ws: InternalWalletState,
+ transactionId: string,
+ ti: TransitionInfo | undefined,
+): void {
+ if (
+ ti &&
+ !(
+ ti.oldTxState.major === ti.newTxState.major &&
+ ti.oldTxState.minor === ti.newTxState.minor
+ )
+ ) {
+ ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ oldTxState: ti.oldTxState,
+ newTxState: ti.newTxState,
+ transactionId,
+ });
+ }
+}
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 3f3eb3784..d1816de03 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -132,6 +132,7 @@ import {
import { PendingTaskType, isWithdrawableDenom } from "../index.js";
import {
constructTransactionIdentifier,
+ notifyTransition,
stopLongpolling,
} from "./transactions.js";
@@ -149,7 +150,7 @@ export async function suspendWithdrawalTransaction(
withdrawalGroupId,
});
stopLongpolling(ws, taskId);
- const stateUpdate = await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
@@ -198,24 +199,18 @@ export async function suspendWithdrawalTransaction(
return undefined;
});
- if (stateUpdate) {
- ws.notify({
- type: NotificationType.TransactionStateTransition,
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- }),
- oldTxState: stateUpdate.oldTxState,
- newTxState: stateUpdate.newTxState,
- });
- }
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
}
export async function resumeWithdrawalTransaction(
ws: InternalWalletState,
withdrawalGroupId: string,
) {
- const stateUpdate = await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
@@ -264,17 +259,11 @@ export async function resumeWithdrawalTransaction(
return undefined;
});
- if (stateUpdate) {
- ws.notify({
- type: NotificationType.TransactionStateTransition,
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- }),
- oldTxState: stateUpdate.oldTxState,
- newTxState: stateUpdate.newTxState,
- });
- }
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
}
export async function abortWithdrawalTransaction(
@@ -285,8 +274,12 @@ export async function abortWithdrawalTransaction(
tag: PendingTaskType.Withdraw,
withdrawalGroupId,
});
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
stopLongpolling(ws, taskId);
- const stateUpdate = await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
@@ -339,18 +332,7 @@ export async function abortWithdrawalTransaction(
}
return undefined;
});
-
- if (stateUpdate) {
- ws.notify({
- type: NotificationType.TransactionStateTransition,
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- }),
- oldTxState: stateUpdate.oldTxState,
- newTxState: stateUpdate.newTxState,
- });
- }
+ notifyTransition(ws, transactionId, transitionInfo);
}
// Called "cancel" in the spec right now,
@@ -363,6 +345,10 @@ export async function cancelAbortingWithdrawalTransaction(
tag: PendingTaskType.Withdraw,
withdrawalGroupId,
});
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
stopLongpolling(ws, taskId);
const stateUpdate = await ws.db
.mktx((x) => [x.withdrawalGroups])
@@ -392,21 +378,9 @@ export async function cancelAbortingWithdrawalTransaction(
}
return undefined;
});
-
- if (stateUpdate) {
- ws.notify({
- type: NotificationType.TransactionStateTransition,
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- }),
- oldTxState: stateUpdate.oldTxState,
- newTxState: stateUpdate.newTxState,
- });
- }
+ notifyTransition(ws, transactionId, stateUpdate);
}
-
export function computeWithdrawalTransactionStatus(
wgRecord: WithdrawalGroupRecord,
): TransactionState {
@@ -1140,6 +1114,10 @@ async function queryReserve(
withdrawalGroupId: string,
cancellationToken: CancellationToken,
): Promise<{ ready: boolean }> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId,
});
@@ -1190,25 +1168,31 @@ async function queryReserve(
logger.trace(`got reserve status ${j2s(result.response)}`);
- await ws.db
+ const transitionResult = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return;
+ return undefined;
}
+ const txStateOld = computeWithdrawalTransactionStatus(wg);
wg.status = WithdrawalGroupStatus.Ready;
+ const txStateNew = computeWithdrawalTransactionStatus(wg);
wg.reserveBalanceAmount = Amounts.stringify(result.response.balance);
await tx.withdrawalGroups.put(wg);
+ return {
+ oldTxState: txStateOld,
+ newTxState: txStateNew,
+ };
});
+ notifyTransition(ws, transactionId, transitionResult);
+
+ // FIXME: This notification is deprecated with DD37
ws.notify({
type: NotificationType.WithdrawalGroupReserveReady,
- transactionId: makeTransactionId(
- TransactionType.Withdrawal,
- withdrawalGroupId,
- ),
+ transactionId,
});
return { ready: true };
@@ -1252,6 +1236,10 @@ export async function processWithdrawalGroup(
}
const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
// We're already running!
if (ws.activeLongpoll[retryTag]) {
@@ -1322,17 +1310,24 @@ export async function processWithdrawalGroup(
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
logger.warn("Finishing empty withdrawal group (no denoms)");
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
- return;
+ return undefined;
}
+ const txStatusOld = computeWithdrawalTransactionStatus(wg);
wg.status = WithdrawalGroupStatus.Finished;
wg.timestampFinish = TalerProtocolTimestamp.now();
+ const txStatusNew = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg);
+ return {
+ oldTxState: txStatusOld,
+ newTxState: txStatusNew,
+ };
});
+ notifyTransition(ws, transactionId, transitionInfo);
return {
type: OperationAttemptResultType.Finished,
result: undefined,
@@ -1421,6 +1416,7 @@ export async function processWithdrawalGroup(
errorsPerCoin[x.coinIdx] = x.lastError;
}
});
+ const oldTxState = computeWithdrawalTransactionStatus(wg);
logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
finishedForFirstTime = true;
@@ -1428,10 +1424,15 @@ export async function processWithdrawalGroup(
wg.status = WithdrawalGroupStatus.Finished;
}
+ const newTxState = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg);
return {
kycInfo: wg.kycPending,
+ transitionInfo: {
+ oldTxState,
+ newTxState,
+ },
};
});
@@ -1439,6 +1440,8 @@ export async function processWithdrawalGroup(
throw Error("withdrawal group does not exist anymore");
}
+ notifyTransition(ws, transactionId, res.transitionInfo);
+
const { kycInfo } = res;
if (numKycRequired > 0) {
@@ -1478,6 +1481,7 @@ export async function processWithdrawalGroup(
);
}
+ // FIXME: Deprecated with DD37
if (finishedForFirstTime) {
ws.notify({
type: NotificationType.WithdrawGroupFinished,
@@ -1838,6 +1842,10 @@ async function registerReserveWithBank(
.runReadOnly(async (tx) => {
return await tx.withdrawalGroups.get(withdrawalGroupId);
});
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
switch (withdrawalGroup?.status) {
case WithdrawalGroupStatus.WaitConfirmBank:
case WithdrawalGroupStatus.RegisteringBank:
@@ -1860,19 +1868,21 @@ async function registerReserveWithBank(
selected_exchange: bankInfo.exchangePaytoUri,
};
logger.info(`registering reserve with bank: ${j2s(reqBody)}`);
- const httpResp = await ws.http.postJson(bankStatusUrl, reqBody, {
+ const httpResp = await ws.http.fetch(bankStatusUrl, {
+ method: "POST",
+ body: reqBody,
timeout: getReserveRequestTimeout(withdrawalGroup),
});
await readSuccessResponseJsonOrThrow(
httpResp,
codecForBankWithdrawalOperationPostResponse(),
);
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const r = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!r) {
- return;
+ return undefined;
}
switch (r.status) {
case WithdrawalGroupStatus.RegisteringBank:
@@ -1887,9 +1897,18 @@ async function registerReserveWithBank(
r.wgInfo.bankInfo.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
AbsoluteTime.now(),
);
+ const oldTxState = computeWithdrawalTransactionStatus(r);
r.status = WithdrawalGroupStatus.WaitConfirmBank;
+ const newTxState = computeWithdrawalTransactionStatus(r);
await tx.withdrawalGroups.put(r);
+ return {
+ oldTxState,
+ newTxState,
+ };
});
+
+ notifyTransition(ws, transactionId, transitionInfo);
+ // FIXME: This notification is deprecated with DD37
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
}
@@ -1904,6 +1923,10 @@ async function processReserveBankStatus(
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId,
});
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
switch (withdrawalGroup?.status) {
case WithdrawalGroupStatus.WaitConfirmBank:
case WithdrawalGroupStatus.RegisteringBank:
@@ -1938,7 +1961,7 @@ async function processReserveBankStatus(
if (status.aborted) {
logger.info("bank aborted the withdrawal");
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const r = await tx.withdrawalGroups.get(withdrawalGroupId);
@@ -1956,10 +1979,17 @@ async function processReserveBankStatus(
throw Error("invariant failed");
}
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
+ const oldTxState = computeWithdrawalTransactionStatus(r);
r.wgInfo.bankInfo.timestampBankConfirmed = now;
r.status = WithdrawalGroupStatus.BankAborted;
+ const newTxState = computeWithdrawalTransactionStatus(r);
await tx.withdrawalGroups.put(r);
+ return {
+ oldTxState,
+ newTxState,
+ }
});
+ notifyTransition(ws, transactionId, transitionInfo);
return {
status: BankStatusResultCode.Aborted,
};
@@ -1977,12 +2007,12 @@ async function processReserveBankStatus(
return await processReserveBankStatus(ws, withdrawalGroupId);
}
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const r = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!r) {
- return;
+ return undefined;
}
// Re-check reserve status within transaction
switch (r.status) {
@@ -1990,16 +2020,18 @@ async function processReserveBankStatus(
case WithdrawalGroupStatus.WaitConfirmBank:
break;
default:
- return;
+ return undefined;
}
if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
throw Error("invariant failed");
}
+ const oldTxState = computeWithdrawalTransactionStatus(r);
if (status.transfer_done) {
logger.info("withdrawal: transfer confirmed by bank.");
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
r.wgInfo.bankInfo.timestampBankConfirmed = now;
r.status = WithdrawalGroupStatus.QueryingStatus;
+ // FIXME: Notification is deprecated with DD37.
ws.notify({
type: NotificationType.WithdrawalGroupBankConfirmed,
transactionId: makeTransactionId(
@@ -2012,9 +2044,16 @@ async function processReserveBankStatus(
r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
r.senderWire = status.sender_wire;
}
+ const newTxState = computeWithdrawalTransactionStatus(r);
await tx.withdrawalGroups.put(r);
+ return {
+ oldTxState,
+ newTxState,
+ }
});
+ notifyTransition(ws, transactionId, transitionInfo);
+
if (status.transfer_done) {
return {
status: BankStatusResultCode.Done,
@@ -2071,6 +2110,11 @@ export async function internalCreateWithdrawalGroup(
withdrawalGroupId = encodeCrock(getRandomBytes(32));
}
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
+
await updateWithdrawalDenoms(ws, canonExchange);
const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
@@ -2122,7 +2166,7 @@ export async function internalCreateWithdrawalGroup(
exchangeInfo.exchange,
);
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [
x.withdrawalGroups,
x.reserves,
@@ -2151,8 +2195,19 @@ export async function internalCreateWithdrawalGroup(
uids: [encodeCrock(getRandomBytes(32))],
});
}
+
+ const oldTxState = {
+ major: TransactionMajorState.None,
+ }
+ const newTxState = computeWithdrawalTransactionStatus(withdrawalGroup);
+ return {
+ oldTxState,
+ newTxState,
+ }
});
+ notifyTransition(ws, transactionId, transitionInfo);
+
return withdrawalGroup;
}
@@ -2225,6 +2280,10 @@ export async function acceptWithdrawalFromUri(
});
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+ const transactionId = constructTaskIdentifier({
+ tag: PendingTaskType.Withdraw,
+ withdrawalGroupId,
+ });
// We do this here, as the reserve should be registered before we return,
// so that we can redirect the user to the bank's status page.
@@ -2249,10 +2308,7 @@ export async function acceptWithdrawalFromUri(
return {
reservePub: withdrawalGroup.reservePub,
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
- transactionId: makeTransactionId(
- TransactionType.Withdrawal,
- withdrawalGroupId,
- ),
+ transactionId,
};
}
@@ -2285,6 +2341,10 @@ export async function createManualWithdrawal(
});
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+ const transactionId = constructTaskIdentifier({
+ tag: PendingTaskType.Withdraw,
+ withdrawalGroupId,
+ });
const exchangePaytoUris = await ws.db
.mktx((x) => [
@@ -2313,9 +2373,6 @@ export async function createManualWithdrawal(
return {
reservePub: withdrawalGroup.reservePub,
exchangePaytoUris: exchangePaytoUris,
- transactionId: makeTransactionId(
- TransactionType.Withdrawal,
- withdrawalGroupId,
- ),
+ transactionId,
};
}
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 9ddf82319..f394aa9ca 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -40,6 +40,7 @@ import {
ApplyRefundResponse,
BackupRecovery,
BalancesResponse,
+ CancelAbortingTransactionRequest,
CheckPeerPullCreditRequest,
CheckPeerPullCreditResponse,
CheckPeerPushDebitRequest,
@@ -156,6 +157,7 @@ export enum WalletApiOperation {
GetExchangeDetailedInfo = "getExchangeDetailedInfo",
RetryPendingNow = "retryPendingNow",
AbortTransaction = "abortTransaction",
+ CancelAbortingTransaction = "cancelAbortingTransaction",
SuspendTransaction = "suspendTransaction",
ResumeTransaction = "resumeTransaction",
ConfirmPay = "confirmPay",
@@ -328,6 +330,17 @@ export type AbortTransactionOp = {
};
/**
+ * Cancel aborting a transaction
+ *
+ * For payment transactions, it puts the payment into an "aborting" state.
+ */
+export type CancelAbortingTransactionOp = {
+ op: WalletApiOperation.CancelAbortingTransaction;
+ request: CancelAbortingTransactionRequest;
+ response: EmptyObject;
+};
+
+/**
* Suspend a transaction
*/
export type SuspendTransactionOp = {
@@ -922,6 +935,7 @@ export type WalletOperations = {
[WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp;
[WalletApiOperation.ConfirmPay]: ConfirmPayOp;
[WalletApiOperation.AbortTransaction]: AbortTransactionOp;
+ [WalletApiOperation.CancelAbortingTransaction]: CancelAbortingTransactionOp;
[WalletApiOperation.SuspendTransaction]: SuspendTransactionOp;
[WalletApiOperation.ResumeTransaction]: ResumeTransactionOp;
[WalletApiOperation.GetBalances]: GetBalancesOp;
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index ab9f43004..733c239f9 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -1221,7 +1221,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
}
case WalletApiOperation.AbortTransaction: {
const req = codecForAbortTransaction().decode(payload);
- await abortTransaction(ws, req.transactionId, req.forceImmediateAbort);
+ await abortTransaction(ws, req.transactionId);
return {};
}
case WalletApiOperation.SuspendTransaction: {