-withdrawal notifications

This commit is contained in:
Florian Dold 2023-05-02 10:59:50 +02:00
parent c4f5c83b8e
commit 16d30adf0d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 205 additions and 95 deletions

View File

@ -1680,21 +1680,15 @@ export const codecForResumeTransaction = (): Codec<ResumeTransactionRequest> =>
export interface AbortTransactionRequest { export interface AbortTransactionRequest {
transactionId: string; transactionId: string;
}
/** export interface CancelAbortingTransactionRequest {
* Move the payment immediately into an aborted state. transactionId: string;
* The UI should warn the user that this might lead
* to money being lost.
*
* Defaults to false.
*/
forceImmediateAbort?: boolean;
} }
export const codecForAbortTransaction = (): Codec<AbortTransactionRequest> => export const codecForAbortTransaction = (): Codec<AbortTransactionRequest> =>
buildCodecForObject<AbortTransactionRequest>() buildCodecForObject<AbortTransactionRequest>()
.property("transactionId", codecForString()) .property("transactionId", codecForString())
.property("forceImmediateAbort", codecOptional(codecForBoolean()))
.build("AbortTransactionRequest"); .build("AbortTransactionRequest");
export interface DepositGroupFees { export interface DepositGroupFees {

View File

@ -443,6 +443,21 @@ transactionsCli
}); });
}); });
transactionsCli
.subcommand("cancelAbortingTransaction", "suspend", {
help: "Cancel the attempt of properly aborting a transaction.",
})
.requiredArgument("transactionId", clk.STRING, {
help: "Identifier of the transaction to cancel aborting.",
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.CancelAbortingTransaction, {
transactionId: args.cancelAbortingTransaction.transactionId,
});
});
});
transactionsCli transactionsCli
.subcommand("resumeTransaction", "resume", { .subcommand("resumeTransaction", "resume", {
help: "Resume a transaction.", help: "Resume a transaction.",
@ -484,14 +499,10 @@ transactionsCli
.requiredArgument("transactionId", clk.STRING, { .requiredArgument("transactionId", clk.STRING, {
help: "Identifier of the transaction to delete", help: "Identifier of the transaction to delete",
}) })
.flag("force", ["--force"], {
help: "Force aborting the transaction. Might lose money.",
})
.action(async (args) => { .action(async (args) => {
await withWallet(args, async (wallet) => { await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.AbortTransaction, { await wallet.client.call(WalletApiOperation.AbortTransaction, {
transactionId: args.abortTransaction.transactionId, transactionId: args.abortTransaction.transactionId,
forceImmediateAbort: args.abortTransaction.force,
}); });
}); });
}); });

View File

@ -26,6 +26,7 @@ import {
ExtendedStatus, ExtendedStatus,
j2s, j2s,
Logger, Logger,
NotificationType,
OrderShortInfo, OrderShortInfo,
PaymentStatus, PaymentStatus,
PeerContractTerms, PeerContractTerms,
@ -38,6 +39,7 @@ import {
TransactionMajorState, TransactionMajorState,
TransactionsRequest, TransactionsRequest,
TransactionsResponse, TransactionsResponse,
TransactionState,
TransactionType, TransactionType,
WithdrawalType, WithdrawalType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
@ -94,6 +96,7 @@ import { processPeerPullCredit } from "./pay-peer.js";
import { processRefreshGroup } from "./refresh.js"; import { processRefreshGroup } from "./refresh.js";
import { computeTipTransactionStatus, processTip } from "./tip.js"; import { computeTipTransactionStatus, processTip } from "./tip.js";
import { import {
abortWithdrawalTransaction,
augmentPaytoUrisForWithdrawal, augmentPaytoUrisForWithdrawal,
computeWithdrawalTransactionStatus, computeWithdrawalTransactionStatus,
processWithdrawalGroup, processWithdrawalGroup,
@ -1854,24 +1857,55 @@ export async function deleteTransaction(
export async function abortTransaction( export async function abortTransaction(
ws: InternalWalletState, ws: InternalWalletState,
transactionId: string, transactionId: string,
forceImmediateAbort?: boolean,
): Promise<void> { ): 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: { case TransactionType.Payment: {
const proposalId = rest[0]; await abortPay(ws, txId.proposalId);
await abortPay(ws, proposalId, forceImmediateAbort);
break; break;
} }
case TransactionType.PeerPushDebit: { case TransactionType.Withdrawal: {
await abortWithdrawalTransaction(ws, txId.withdrawalGroupId);
break; break;
} }
default: { default: {
const unknownTxType: any = type; const unknownTxType: any = txId.tag;
throw Error( throw Error(
`can't abort a '${unknownTxType}' transaction: not yet implemented`, `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,
});
}
}

View File

@ -132,6 +132,7 @@ import {
import { PendingTaskType, isWithdrawableDenom } from "../index.js"; import { PendingTaskType, isWithdrawableDenom } from "../index.js";
import { import {
constructTransactionIdentifier, constructTransactionIdentifier,
notifyTransition,
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
@ -149,7 +150,7 @@ export async function suspendWithdrawalTransaction(
withdrawalGroupId, withdrawalGroupId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const stateUpdate = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups]) .mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId); const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
@ -198,24 +199,18 @@ export async function suspendWithdrawalTransaction(
return undefined; return undefined;
}); });
if (stateUpdate) { const transactionId = constructTransactionIdentifier({
ws.notify({ tag: TransactionType.Withdrawal,
type: NotificationType.TransactionStateTransition, withdrawalGroupId,
transactionId: constructTransactionIdentifier({ });
tag: TransactionType.Withdrawal, notifyTransition(ws, transactionId, transitionInfo);
withdrawalGroupId,
}),
oldTxState: stateUpdate.oldTxState,
newTxState: stateUpdate.newTxState,
});
}
} }
export async function resumeWithdrawalTransaction( export async function resumeWithdrawalTransaction(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalGroupId: string, withdrawalGroupId: string,
) { ) {
const stateUpdate = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups]) .mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId); const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
@ -264,17 +259,11 @@ export async function resumeWithdrawalTransaction(
return undefined; return undefined;
}); });
if (stateUpdate) { const transactionId = constructTransactionIdentifier({
ws.notify({ tag: TransactionType.Withdrawal,
type: NotificationType.TransactionStateTransition, withdrawalGroupId,
transactionId: constructTransactionIdentifier({ });
tag: TransactionType.Withdrawal, notifyTransition(ws, transactionId, transitionInfo);
withdrawalGroupId,
}),
oldTxState: stateUpdate.oldTxState,
newTxState: stateUpdate.newTxState,
});
}
} }
export async function abortWithdrawalTransaction( export async function abortWithdrawalTransaction(
@ -285,8 +274,12 @@ export async function abortWithdrawalTransaction(
tag: PendingTaskType.Withdraw, tag: PendingTaskType.Withdraw,
withdrawalGroupId, withdrawalGroupId,
}); });
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const stateUpdate = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups]) .mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId); const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
@ -339,18 +332,7 @@ export async function abortWithdrawalTransaction(
} }
return undefined; return undefined;
}); });
notifyTransition(ws, transactionId, transitionInfo);
if (stateUpdate) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId: constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
}),
oldTxState: stateUpdate.oldTxState,
newTxState: stateUpdate.newTxState,
});
}
} }
// Called "cancel" in the spec right now, // Called "cancel" in the spec right now,
@ -363,6 +345,10 @@ export async function cancelAbortingWithdrawalTransaction(
tag: PendingTaskType.Withdraw, tag: PendingTaskType.Withdraw,
withdrawalGroupId, withdrawalGroupId,
}); });
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const stateUpdate = await ws.db const stateUpdate = await ws.db
.mktx((x) => [x.withdrawalGroups]) .mktx((x) => [x.withdrawalGroups])
@ -392,21 +378,9 @@ export async function cancelAbortingWithdrawalTransaction(
} }
return undefined; return undefined;
}); });
notifyTransition(ws, transactionId, stateUpdate);
if (stateUpdate) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId: constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
}),
oldTxState: stateUpdate.oldTxState,
newTxState: stateUpdate.newTxState,
});
}
} }
export function computeWithdrawalTransactionStatus( export function computeWithdrawalTransactionStatus(
wgRecord: WithdrawalGroupRecord, wgRecord: WithdrawalGroupRecord,
): TransactionState { ): TransactionState {
@ -1140,6 +1114,10 @@ async function queryReserve(
withdrawalGroupId: string, withdrawalGroupId: string,
cancellationToken: CancellationToken, cancellationToken: CancellationToken,
): Promise<{ ready: boolean }> { ): Promise<{ ready: boolean }> {
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, { const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId, withdrawalGroupId,
}); });
@ -1190,25 +1168,31 @@ async function queryReserve(
logger.trace(`got reserve status ${j2s(result.response)}`); logger.trace(`got reserve status ${j2s(result.response)}`);
await ws.db const transitionResult = await ws.db
.mktx((x) => [x.withdrawalGroups]) .mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId); const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) { if (!wg) {
logger.warn(`withdrawal group ${withdrawalGroupId} not found`); logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
return; return undefined;
} }
const txStateOld = computeWithdrawalTransactionStatus(wg);
wg.status = WithdrawalGroupStatus.Ready; wg.status = WithdrawalGroupStatus.Ready;
const txStateNew = computeWithdrawalTransactionStatus(wg);
wg.reserveBalanceAmount = Amounts.stringify(result.response.balance); wg.reserveBalanceAmount = Amounts.stringify(result.response.balance);
await tx.withdrawalGroups.put(wg); await tx.withdrawalGroups.put(wg);
return {
oldTxState: txStateOld,
newTxState: txStateNew,
};
}); });
notifyTransition(ws, transactionId, transitionResult);
// FIXME: This notification is deprecated with DD37
ws.notify({ ws.notify({
type: NotificationType.WithdrawalGroupReserveReady, type: NotificationType.WithdrawalGroupReserveReady,
transactionId: makeTransactionId( transactionId,
TransactionType.Withdrawal,
withdrawalGroupId,
),
}); });
return { ready: true }; return { ready: true };
@ -1252,6 +1236,10 @@ export async function processWithdrawalGroup(
} }
const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup); const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
// We're already running! // We're already running!
if (ws.activeLongpoll[retryTag]) { if (ws.activeLongpoll[retryTag]) {
@ -1322,17 +1310,24 @@ export async function processWithdrawalGroup(
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) { if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
logger.warn("Finishing empty withdrawal group (no denoms)"); logger.warn("Finishing empty withdrawal group (no denoms)");
await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups]) .mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId); const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) { if (!wg) {
return; return undefined;
} }
const txStatusOld = computeWithdrawalTransactionStatus(wg);
wg.status = WithdrawalGroupStatus.Finished; wg.status = WithdrawalGroupStatus.Finished;
wg.timestampFinish = TalerProtocolTimestamp.now(); wg.timestampFinish = TalerProtocolTimestamp.now();
const txStatusNew = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg); await tx.withdrawalGroups.put(wg);
return {
oldTxState: txStatusOld,
newTxState: txStatusNew,
};
}); });
notifyTransition(ws, transactionId, transitionInfo);
return { return {
type: OperationAttemptResultType.Finished, type: OperationAttemptResultType.Finished,
result: undefined, result: undefined,
@ -1421,6 +1416,7 @@ export async function processWithdrawalGroup(
errorsPerCoin[x.coinIdx] = x.lastError; errorsPerCoin[x.coinIdx] = x.lastError;
} }
}); });
const oldTxState = computeWithdrawalTransactionStatus(wg);
logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`); logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) { if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
finishedForFirstTime = true; finishedForFirstTime = true;
@ -1428,10 +1424,15 @@ export async function processWithdrawalGroup(
wg.status = WithdrawalGroupStatus.Finished; wg.status = WithdrawalGroupStatus.Finished;
} }
const newTxState = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg); await tx.withdrawalGroups.put(wg);
return { return {
kycInfo: wg.kycPending, kycInfo: wg.kycPending,
transitionInfo: {
oldTxState,
newTxState,
},
}; };
}); });
@ -1439,6 +1440,8 @@ export async function processWithdrawalGroup(
throw Error("withdrawal group does not exist anymore"); throw Error("withdrawal group does not exist anymore");
} }
notifyTransition(ws, transactionId, res.transitionInfo);
const { kycInfo } = res; const { kycInfo } = res;
if (numKycRequired > 0) { if (numKycRequired > 0) {
@ -1478,6 +1481,7 @@ export async function processWithdrawalGroup(
); );
} }
// FIXME: Deprecated with DD37
if (finishedForFirstTime) { if (finishedForFirstTime) {
ws.notify({ ws.notify({
type: NotificationType.WithdrawGroupFinished, type: NotificationType.WithdrawGroupFinished,
@ -1838,6 +1842,10 @@ async function registerReserveWithBank(
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return await tx.withdrawalGroups.get(withdrawalGroupId); return await tx.withdrawalGroups.get(withdrawalGroupId);
}); });
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
switch (withdrawalGroup?.status) { switch (withdrawalGroup?.status) {
case WithdrawalGroupStatus.WaitConfirmBank: case WithdrawalGroupStatus.WaitConfirmBank:
case WithdrawalGroupStatus.RegisteringBank: case WithdrawalGroupStatus.RegisteringBank:
@ -1860,19 +1868,21 @@ async function registerReserveWithBank(
selected_exchange: bankInfo.exchangePaytoUri, selected_exchange: bankInfo.exchangePaytoUri,
}; };
logger.info(`registering reserve with bank: ${j2s(reqBody)}`); 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), timeout: getReserveRequestTimeout(withdrawalGroup),
}); });
await readSuccessResponseJsonOrThrow( await readSuccessResponseJsonOrThrow(
httpResp, httpResp,
codecForBankWithdrawalOperationPostResponse(), codecForBankWithdrawalOperationPostResponse(),
); );
await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups]) .mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const r = await tx.withdrawalGroups.get(withdrawalGroupId); const r = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!r) { if (!r) {
return; return undefined;
} }
switch (r.status) { switch (r.status) {
case WithdrawalGroupStatus.RegisteringBank: case WithdrawalGroupStatus.RegisteringBank:
@ -1887,9 +1897,18 @@ async function registerReserveWithBank(
r.wgInfo.bankInfo.timestampReserveInfoPosted = AbsoluteTime.toTimestamp( r.wgInfo.bankInfo.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
AbsoluteTime.now(), AbsoluteTime.now(),
); );
const oldTxState = computeWithdrawalTransactionStatus(r);
r.status = WithdrawalGroupStatus.WaitConfirmBank; r.status = WithdrawalGroupStatus.WaitConfirmBank;
const newTxState = computeWithdrawalTransactionStatus(r);
await tx.withdrawalGroups.put(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 }); ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
} }
@ -1904,6 +1923,10 @@ async function processReserveBankStatus(
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, { const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId, withdrawalGroupId,
}); });
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
switch (withdrawalGroup?.status) { switch (withdrawalGroup?.status) {
case WithdrawalGroupStatus.WaitConfirmBank: case WithdrawalGroupStatus.WaitConfirmBank:
case WithdrawalGroupStatus.RegisteringBank: case WithdrawalGroupStatus.RegisteringBank:
@ -1938,7 +1961,7 @@ async function processReserveBankStatus(
if (status.aborted) { if (status.aborted) {
logger.info("bank aborted the withdrawal"); logger.info("bank aborted the withdrawal");
await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups]) .mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const r = await tx.withdrawalGroups.get(withdrawalGroupId); const r = await tx.withdrawalGroups.get(withdrawalGroupId);
@ -1956,10 +1979,17 @@ async function processReserveBankStatus(
throw Error("invariant failed"); throw Error("invariant failed");
} }
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now()); const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
const oldTxState = computeWithdrawalTransactionStatus(r);
r.wgInfo.bankInfo.timestampBankConfirmed = now; r.wgInfo.bankInfo.timestampBankConfirmed = now;
r.status = WithdrawalGroupStatus.BankAborted; r.status = WithdrawalGroupStatus.BankAborted;
const newTxState = computeWithdrawalTransactionStatus(r);
await tx.withdrawalGroups.put(r); await tx.withdrawalGroups.put(r);
return {
oldTxState,
newTxState,
}
}); });
notifyTransition(ws, transactionId, transitionInfo);
return { return {
status: BankStatusResultCode.Aborted, status: BankStatusResultCode.Aborted,
}; };
@ -1977,12 +2007,12 @@ async function processReserveBankStatus(
return await processReserveBankStatus(ws, withdrawalGroupId); return await processReserveBankStatus(ws, withdrawalGroupId);
} }
await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups]) .mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const r = await tx.withdrawalGroups.get(withdrawalGroupId); const r = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!r) { if (!r) {
return; return undefined;
} }
// Re-check reserve status within transaction // Re-check reserve status within transaction
switch (r.status) { switch (r.status) {
@ -1990,16 +2020,18 @@ async function processReserveBankStatus(
case WithdrawalGroupStatus.WaitConfirmBank: case WithdrawalGroupStatus.WaitConfirmBank:
break; break;
default: default:
return; return undefined;
} }
if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) { if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
throw Error("invariant failed"); throw Error("invariant failed");
} }
const oldTxState = computeWithdrawalTransactionStatus(r);
if (status.transfer_done) { if (status.transfer_done) {
logger.info("withdrawal: transfer confirmed by bank."); logger.info("withdrawal: transfer confirmed by bank.");
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now()); const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
r.wgInfo.bankInfo.timestampBankConfirmed = now; r.wgInfo.bankInfo.timestampBankConfirmed = now;
r.status = WithdrawalGroupStatus.QueryingStatus; r.status = WithdrawalGroupStatus.QueryingStatus;
// FIXME: Notification is deprecated with DD37.
ws.notify({ ws.notify({
type: NotificationType.WithdrawalGroupBankConfirmed, type: NotificationType.WithdrawalGroupBankConfirmed,
transactionId: makeTransactionId( transactionId: makeTransactionId(
@ -2012,9 +2044,16 @@ async function processReserveBankStatus(
r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url; r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
r.senderWire = status.sender_wire; r.senderWire = status.sender_wire;
} }
const newTxState = computeWithdrawalTransactionStatus(r);
await tx.withdrawalGroups.put(r); await tx.withdrawalGroups.put(r);
return {
oldTxState,
newTxState,
}
}); });
notifyTransition(ws, transactionId, transitionInfo);
if (status.transfer_done) { if (status.transfer_done) {
return { return {
status: BankStatusResultCode.Done, status: BankStatusResultCode.Done,
@ -2071,6 +2110,11 @@ export async function internalCreateWithdrawalGroup(
withdrawalGroupId = encodeCrock(getRandomBytes(32)); withdrawalGroupId = encodeCrock(getRandomBytes(32));
} }
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
await updateWithdrawalDenoms(ws, canonExchange); await updateWithdrawalDenoms(ws, canonExchange);
const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange); const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
@ -2122,7 +2166,7 @@ export async function internalCreateWithdrawalGroup(
exchangeInfo.exchange, exchangeInfo.exchange,
); );
await ws.db const transitionInfo = await ws.db
.mktx((x) => [ .mktx((x) => [
x.withdrawalGroups, x.withdrawalGroups,
x.reserves, x.reserves,
@ -2151,8 +2195,19 @@ export async function internalCreateWithdrawalGroup(
uids: [encodeCrock(getRandomBytes(32))], uids: [encodeCrock(getRandomBytes(32))],
}); });
} }
const oldTxState = {
major: TransactionMajorState.None,
}
const newTxState = computeWithdrawalTransactionStatus(withdrawalGroup);
return {
oldTxState,
newTxState,
}
}); });
notifyTransition(ws, transactionId, transitionInfo);
return withdrawalGroup; return withdrawalGroup;
} }
@ -2225,6 +2280,10 @@ export async function acceptWithdrawalFromUri(
}); });
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
const transactionId = constructTaskIdentifier({
tag: PendingTaskType.Withdraw,
withdrawalGroupId,
});
// We do this here, as the reserve should be registered before we return, // 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. // so that we can redirect the user to the bank's status page.
@ -2249,10 +2308,7 @@ export async function acceptWithdrawalFromUri(
return { return {
reservePub: withdrawalGroup.reservePub, reservePub: withdrawalGroup.reservePub,
confirmTransferUrl: withdrawInfo.confirmTransferUrl, confirmTransferUrl: withdrawInfo.confirmTransferUrl,
transactionId: makeTransactionId( transactionId,
TransactionType.Withdrawal,
withdrawalGroupId,
),
}; };
} }
@ -2285,6 +2341,10 @@ export async function createManualWithdrawal(
}); });
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
const transactionId = constructTaskIdentifier({
tag: PendingTaskType.Withdraw,
withdrawalGroupId,
});
const exchangePaytoUris = await ws.db const exchangePaytoUris = await ws.db
.mktx((x) => [ .mktx((x) => [
@ -2313,9 +2373,6 @@ export async function createManualWithdrawal(
return { return {
reservePub: withdrawalGroup.reservePub, reservePub: withdrawalGroup.reservePub,
exchangePaytoUris: exchangePaytoUris, exchangePaytoUris: exchangePaytoUris,
transactionId: makeTransactionId( transactionId,
TransactionType.Withdrawal,
withdrawalGroupId,
),
}; };
} }

View File

@ -40,6 +40,7 @@ import {
ApplyRefundResponse, ApplyRefundResponse,
BackupRecovery, BackupRecovery,
BalancesResponse, BalancesResponse,
CancelAbortingTransactionRequest,
CheckPeerPullCreditRequest, CheckPeerPullCreditRequest,
CheckPeerPullCreditResponse, CheckPeerPullCreditResponse,
CheckPeerPushDebitRequest, CheckPeerPushDebitRequest,
@ -156,6 +157,7 @@ export enum WalletApiOperation {
GetExchangeDetailedInfo = "getExchangeDetailedInfo", GetExchangeDetailedInfo = "getExchangeDetailedInfo",
RetryPendingNow = "retryPendingNow", RetryPendingNow = "retryPendingNow",
AbortTransaction = "abortTransaction", AbortTransaction = "abortTransaction",
CancelAbortingTransaction = "cancelAbortingTransaction",
SuspendTransaction = "suspendTransaction", SuspendTransaction = "suspendTransaction",
ResumeTransaction = "resumeTransaction", ResumeTransaction = "resumeTransaction",
ConfirmPay = "confirmPay", ConfirmPay = "confirmPay",
@ -327,6 +329,17 @@ export type AbortTransactionOp = {
response: EmptyObject; response: EmptyObject;
}; };
/**
* 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 * Suspend a transaction
*/ */
@ -922,6 +935,7 @@ export type WalletOperations = {
[WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp; [WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp;
[WalletApiOperation.ConfirmPay]: ConfirmPayOp; [WalletApiOperation.ConfirmPay]: ConfirmPayOp;
[WalletApiOperation.AbortTransaction]: AbortTransactionOp; [WalletApiOperation.AbortTransaction]: AbortTransactionOp;
[WalletApiOperation.CancelAbortingTransaction]: CancelAbortingTransactionOp;
[WalletApiOperation.SuspendTransaction]: SuspendTransactionOp; [WalletApiOperation.SuspendTransaction]: SuspendTransactionOp;
[WalletApiOperation.ResumeTransaction]: ResumeTransactionOp; [WalletApiOperation.ResumeTransaction]: ResumeTransactionOp;
[WalletApiOperation.GetBalances]: GetBalancesOp; [WalletApiOperation.GetBalances]: GetBalancesOp;

View File

@ -1221,7 +1221,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
} }
case WalletApiOperation.AbortTransaction: { case WalletApiOperation.AbortTransaction: {
const req = codecForAbortTransaction().decode(payload); const req = codecForAbortTransaction().decode(payload);
await abortTransaction(ws, req.transactionId, req.forceImmediateAbort); await abortTransaction(ws, req.transactionId);
return {}; return {};
} }
case WalletApiOperation.SuspendTransaction: { case WalletApiOperation.SuspendTransaction: {