summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/reward.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/reward.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/reward.ts387
1 files changed, 217 insertions, 170 deletions
diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts
index 79beb6432..62ac81d7f 100644
--- a/packages/taler-wallet-core/src/operations/reward.ts
+++ b/packages/taler-wallet-core/src/operations/reward.ts
@@ -68,6 +68,8 @@ import {
makeCoinsVisible,
TaskRunResult,
TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
} from "./common.js";
import { fetchFreshExchange } from "./exchanges.js";
import {
@@ -86,6 +88,202 @@ import { assertUnreachable } from "../util/assertUnreachable.js";
const logger = new Logger("operations/tip.ts");
+export class RewardTransactionContext implements TransactionContext {
+ public transactionId: string;
+ public retryTag: string;
+
+ constructor(
+ public ws: InternalWalletState,
+ public walletRewardId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Reward,
+ walletRewardId,
+ });
+ this.retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.RewardPickup,
+ walletRewardId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { ws, walletRewardId } = this;
+ await ws.db
+ .mktx((x) => [x.rewards, x.tombstones])
+ .runReadWrite(async (tx) => {
+ const tipRecord = await tx.rewards.get(walletRewardId);
+ if (tipRecord) {
+ await tx.rewards.delete(walletRewardId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteReward + ":" + walletRewardId,
+ });
+ }
+ });
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { ws, walletRewardId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.rewards])
+ .runReadWrite(async (tx) => {
+ const tipRec = await tx.rewards.get(walletRewardId);
+ if (!tipRec) {
+ logger.warn(`transaction tip ${walletRewardId} not found`);
+ return;
+ }
+ let newStatus: RewardRecordStatus | undefined = undefined;
+ switch (tipRec.status) {
+ case RewardRecordStatus.Done:
+ case RewardRecordStatus.SuspendedPickup:
+ case RewardRecordStatus.Aborted:
+ case RewardRecordStatus.DialogAccept:
+ case RewardRecordStatus.Failed:
+ break;
+ case RewardRecordStatus.PendingPickup:
+ newStatus = RewardRecordStatus.SuspendedPickup;
+ break;
+
+ default:
+ assertUnreachable(tipRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computeRewardTransactionStatus(tipRec);
+ tipRec.status = newStatus;
+ const newTxState = computeRewardTransactionStatus(tipRec);
+ await tx.rewards.put(tipRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ ws.workAvailable.trigger();
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { ws, walletRewardId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.rewards])
+ .runReadWrite(async (tx) => {
+ const tipRec = await tx.rewards.get(walletRewardId);
+ if (!tipRec) {
+ logger.warn(`transaction tip ${walletRewardId} not found`);
+ return;
+ }
+ let newStatus: RewardRecordStatus | undefined = undefined;
+ switch (tipRec.status) {
+ case RewardRecordStatus.Done:
+ case RewardRecordStatus.Aborted:
+ case RewardRecordStatus.PendingPickup:
+ case RewardRecordStatus.DialogAccept:
+ case RewardRecordStatus.Failed:
+ break;
+ case RewardRecordStatus.SuspendedPickup:
+ newStatus = RewardRecordStatus.Aborted;
+ break;
+ default:
+ assertUnreachable(tipRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computeRewardTransactionStatus(tipRec);
+ tipRec.status = newStatus;
+ const newTxState = computeRewardTransactionStatus(tipRec);
+ await tx.rewards.put(tipRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { ws, walletRewardId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.rewards])
+ .runReadWrite(async (tx) => {
+ const rewardRec = await tx.rewards.get(walletRewardId);
+ if (!rewardRec) {
+ logger.warn(`transaction reward ${walletRewardId} not found`);
+ return;
+ }
+ let newStatus: RewardRecordStatus | undefined = undefined;
+ switch (rewardRec.status) {
+ case RewardRecordStatus.Done:
+ case RewardRecordStatus.PendingPickup:
+ case RewardRecordStatus.Aborted:
+ case RewardRecordStatus.DialogAccept:
+ case RewardRecordStatus.Failed:
+ break;
+ case RewardRecordStatus.SuspendedPickup:
+ newStatus = RewardRecordStatus.PendingPickup;
+ break;
+ default:
+ assertUnreachable(rewardRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computeRewardTransactionStatus(rewardRec);
+ rewardRec.status = newStatus;
+ const newTxState = computeRewardTransactionStatus(rewardRec);
+ await tx.rewards.put(rewardRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { ws, walletRewardId, transactionId, retryTag } = this;
+ stopLongpolling(ws, retryTag);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.rewards])
+ .runReadWrite(async (tx) => {
+ const tipRec = await tx.rewards.get(walletRewardId);
+ if (!tipRec) {
+ logger.warn(`transaction tip ${walletRewardId} not found`);
+ return;
+ }
+ let newStatus: RewardRecordStatus | undefined = undefined;
+ switch (tipRec.status) {
+ case RewardRecordStatus.Done:
+ case RewardRecordStatus.Aborted:
+ case RewardRecordStatus.Failed:
+ break;
+ case RewardRecordStatus.PendingPickup:
+ case RewardRecordStatus.DialogAccept:
+ case RewardRecordStatus.SuspendedPickup:
+ newStatus = RewardRecordStatus.Failed;
+ break;
+ default:
+ assertUnreachable(tipRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computeRewardTransactionStatus(tipRec);
+ tipRec.status = newStatus;
+ const newTxState = computeRewardTransactionStatus(tipRec);
+ await tx.rewards.put(tipRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+}
+
/**
* Get the (DD37-style) transaction status based on the
* database record of a reward.
@@ -117,6 +315,10 @@ export function computeRewardTransactionStatus(
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Pickup,
};
+ case RewardRecordStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
default:
assertUnreachable(tipRecord.status);
}
@@ -128,6 +330,8 @@ export function computeTipTransactionActions(
switch (tipRecord.status) {
case RewardRecordStatus.Done:
return [TransactionAction.Delete];
+ case RewardRecordStatus.Failed:
+ return [TransactionAction.Delete];
case RewardRecordStatus.Aborted:
return [TransactionAction.Delete];
case RewardRecordStatus.PendingPickup:
@@ -141,7 +345,7 @@ export function computeTipTransactionActions(
}
}
-export async function prepareTip(
+export async function prepareReward(
ws: InternalWalletState,
talerTipUri: string,
): Promise<PrepareTipResult> {
@@ -166,33 +370,33 @@ export async function prepareTip(
);
logger.trace("checking tip status from", tipStatusUrl.href);
const merchantResp = await ws.http.fetch(tipStatusUrl.href);
- const tipPickupStatus = await readSuccessResponseJsonOrThrow(
+ const rewardPickupStatus = await readSuccessResponseJsonOrThrow(
merchantResp,
codecForRewardPickupGetResponse(),
);
- logger.trace(`status ${j2s(tipPickupStatus)}`);
+ logger.trace(`status ${j2s(rewardPickupStatus)}`);
- const amount = Amounts.parseOrThrow(tipPickupStatus.reward_amount);
+ const amount = Amounts.parseOrThrow(rewardPickupStatus.reward_amount);
const currency = amount.currency;
logger.trace("new tip, creating tip record");
- await fetchFreshExchange(ws, tipPickupStatus.exchange_url);
+ await fetchFreshExchange(ws, rewardPickupStatus.exchange_url);
//FIXME: is this needed? withdrawDetails is not used
// * if the intention is to update the exchange information in the database
// maybe we can use another name. `get` seems like a pure-function
const withdrawDetails = await getExchangeWithdrawalInfo(
ws,
- tipPickupStatus.exchange_url,
+ rewardPickupStatus.exchange_url,
amount,
undefined,
);
- const walletTipId = encodeCrock(getRandomBytes(32));
- await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
+ const walletRewardId = encodeCrock(getRandomBytes(32));
+ await updateWithdrawalDenoms(ws, rewardPickupStatus.exchange_url);
const denoms = await getCandidateWithdrawalDenoms(
ws,
- tipPickupStatus.exchange_url,
+ rewardPickupStatus.exchange_url,
currency,
);
const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
@@ -201,13 +405,13 @@ export async function prepareTip(
const denomSelUid = encodeCrock(getRandomBytes(32));
const newTipRecord: RewardRecord = {
- walletRewardId: walletTipId,
+ walletRewardId: walletRewardId,
acceptedTimestamp: undefined,
status: RewardRecordStatus.DialogAccept,
rewardAmountRaw: Amounts.stringify(amount),
- rewardExpiration: timestampProtocolToDb(tipPickupStatus.expiration),
- exchangeBaseUrl: tipPickupStatus.exchange_url,
- next_url: tipPickupStatus.next_url,
+ rewardExpiration: timestampProtocolToDb(rewardPickupStatus.expiration),
+ exchangeBaseUrl: rewardPickupStatus.exchange_url,
+ next_url: rewardPickupStatus.next_url,
merchantBaseUrl: res.merchantBaseUrl,
createdTimestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
merchantRewardId: res.merchantRewardId,
@@ -485,160 +689,3 @@ export async function acceptTip(
next_url: tipRecord.next_url,
};
}
-
-export async function suspendRewardTransaction(
- ws: InternalWalletState,
- walletRewardId: string,
-): Promise<void> {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.RewardPickup,
- walletRewardId: walletRewardId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Reward,
- walletRewardId: walletRewardId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.rewards])
- .runReadWrite(async (tx) => {
- const tipRec = await tx.rewards.get(walletRewardId);
- if (!tipRec) {
- logger.warn(`transaction tip ${walletRewardId} not found`);
- return;
- }
- let newStatus: RewardRecordStatus | undefined = undefined;
- switch (tipRec.status) {
- case RewardRecordStatus.Done:
- case RewardRecordStatus.SuspendedPickup:
- case RewardRecordStatus.Aborted:
- case RewardRecordStatus.DialogAccept:
- break;
- case RewardRecordStatus.PendingPickup:
- newStatus = RewardRecordStatus.SuspendedPickup;
- break;
-
- default:
- assertUnreachable(tipRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computeRewardTransactionStatus(tipRec);
- tipRec.status = newStatus;
- const newTxState = computeRewardTransactionStatus(tipRec);
- await tx.rewards.put(tipRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- ws.workAvailable.trigger();
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumeTipTransaction(
- ws: InternalWalletState,
- walletRewardId: string,
-): Promise<void> {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.RewardPickup,
- walletRewardId: walletRewardId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Reward,
- walletRewardId: walletRewardId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.rewards])
- .runReadWrite(async (tx) => {
- const rewardRec = await tx.rewards.get(walletRewardId);
- if (!rewardRec) {
- logger.warn(`transaction reward ${walletRewardId} not found`);
- return;
- }
- let newStatus: RewardRecordStatus | undefined = undefined;
- switch (rewardRec.status) {
- case RewardRecordStatus.Done:
- case RewardRecordStatus.PendingPickup:
- case RewardRecordStatus.Aborted:
- case RewardRecordStatus.DialogAccept:
- break;
- case RewardRecordStatus.SuspendedPickup:
- newStatus = RewardRecordStatus.PendingPickup;
- break;
- default:
- assertUnreachable(rewardRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computeRewardTransactionStatus(rewardRec);
- rewardRec.status = newStatus;
- const newTxState = computeRewardTransactionStatus(rewardRec);
- await tx.rewards.put(rewardRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failTipTransaction(
- ws: InternalWalletState,
- walletTipId: string,
-): Promise<void> {
- // We don't have an "aborting" state, so this should never happen!
- throw Error("can't run cance-aborting on tip transaction");
-}
-
-export async function abortTipTransaction(
- ws: InternalWalletState,
- walletRewardId: string,
-): Promise<void> {
- const taskId = constructTaskIdentifier({
- tag: PendingTaskType.RewardPickup,
- walletRewardId: walletRewardId,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Reward,
- walletRewardId: walletRewardId,
- });
- stopLongpolling(ws, taskId);
- const transitionInfo = await ws.db
- .mktx((x) => [x.rewards])
- .runReadWrite(async (tx) => {
- const tipRec = await tx.rewards.get(walletRewardId);
- if (!tipRec) {
- logger.warn(`transaction tip ${walletRewardId} not found`);
- return;
- }
- let newStatus: RewardRecordStatus | undefined = undefined;
- switch (tipRec.status) {
- case RewardRecordStatus.Done:
- case RewardRecordStatus.Aborted:
- case RewardRecordStatus.PendingPickup:
- case RewardRecordStatus.DialogAccept:
- break;
- case RewardRecordStatus.SuspendedPickup:
- newStatus = RewardRecordStatus.Aborted;
- break;
- default:
- assertUnreachable(tipRec.status);
- }
- if (newStatus != null) {
- const oldTxState = computeRewardTransactionStatus(tipRec);
- tipRec.status = newStatus;
- const newTxState = computeRewardTransactionStatus(tipRec);
- await tx.rewards.put(tipRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- });
- notifyTransition(ws, transactionId, transitionInfo);
-}