diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/reward.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/reward.ts | 387 |
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); -} |