diff options
author | Florian Dold <florian@dold.me> | 2023-08-03 18:35:07 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-08-03 18:35:07 +0200 |
commit | fdbd55d2bde0961a4c1ff26b04e442459ab782b0 (patch) | |
tree | d0d04f42a5477f6d7d39a8940d59ff1548166711 /packages/taler-wallet-core/src/operations/tip.ts | |
parent | 0fe4840ca2612dda06417cdebe5229eea98180be (diff) | |
download | wallet-core-fdbd55d2bde0961a4c1ff26b04e442459ab782b0.tar.gz wallet-core-fdbd55d2bde0961a4c1ff26b04e442459ab782b0.tar.bz2 wallet-core-fdbd55d2bde0961a4c1ff26b04e442459ab782b0.zip |
-towards tip->reward rename
Diffstat (limited to 'packages/taler-wallet-core/src/operations/tip.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/tip.ts | 630 |
1 files changed, 0 insertions, 630 deletions
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts deleted file mode 100644 index e56fb1e8d..000000000 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ /dev/null @@ -1,630 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - AcceptTipResponse, - AgeRestriction, - Amounts, - BlindedDenominationSignature, - codecForMerchantTipResponseV2, - codecForTipPickupGetResponse, - CoinStatus, - DenomKeyType, - encodeCrock, - getRandomBytes, - j2s, - Logger, - NotificationType, - parseTipUri, - PrepareTipResult, - TalerErrorCode, - TalerPreciseTimestamp, - TipPlanchetDetail, - TransactionAction, - TransactionMajorState, - TransactionMinorState, - TransactionState, - TransactionType, - URL, -} from "@gnu-taler/taler-util"; -import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js"; -import { - CoinRecord, - CoinSourceType, - DenominationRecord, - TipRecord, - TipRecordStatus, -} from "../db.js"; -import { makeErrorDetail } from "@gnu-taler/taler-util"; -import { InternalWalletState } from "../internal-wallet-state.js"; -import { - getHttpResponseErrorDetails, - readSuccessResponseJsonOrThrow, -} from "@gnu-taler/taler-util/http"; -import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; -import { - constructTaskIdentifier, - makeCoinAvailable, - makeCoinsVisible, - TaskRunResult, - TaskRunResultType, -} from "./common.js"; -import { updateExchangeFromUrl } from "./exchanges.js"; -import { - getCandidateWithdrawalDenoms, - getExchangeWithdrawalInfo, - updateWithdrawalDenoms, -} from "./withdraw.js"; -import { selectWithdrawalDenominations } from "../util/coinSelection.js"; -import { - constructTransactionIdentifier, - notifyTransition, - stopLongpolling, -} from "./transactions.js"; -import { PendingTaskType } from "../pending-types.js"; -import { assertUnreachable } from "../util/assertUnreachable.js"; - -const logger = new Logger("operations/tip.ts"); - -/** - * Get the (DD37-style) transaction status based on the - * database record of a tip. - */ -export function computeTipTransactionStatus( - tipRecord: TipRecord, -): TransactionState { - switch (tipRecord.status) { - case TipRecordStatus.Done: - return { - major: TransactionMajorState.Done, - }; - case TipRecordStatus.Aborted: - return { - major: TransactionMajorState.Aborted, - }; - case TipRecordStatus.PendingPickup: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.Pickup, - }; - case TipRecordStatus.DialogAccept: - return { - major: TransactionMajorState.Dialog, - minor: TransactionMinorState.Proposed, - }; - case TipRecordStatus.SuspendidPickup: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.Pickup, - }; - default: - assertUnreachable(tipRecord.status); - } -} - -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]; - case TipRecordStatus.DialogAccept: - return [TransactionAction.Abort]; - default: - assertUnreachable(tipRecord.status); - } -} - -export async function prepareTip( - ws: InternalWalletState, - talerTipUri: string, -): Promise<PrepareTipResult> { - const res = parseTipUri(talerTipUri); - if (!res) { - throw Error("invalid taler://tip URI"); - } - - let tipRecord = await ws.db - .mktx((x) => [x.tips]) - .runReadOnly(async (tx) => { - return tx.tips.indexes.byMerchantTipIdAndBaseUrl.get([ - res.merchantTipId, - res.merchantBaseUrl, - ]); - }); - - if (!tipRecord) { - const tipStatusUrl = new URL( - `tips/${res.merchantTipId}`, - res.merchantBaseUrl, - ); - logger.trace("checking tip status from", tipStatusUrl.href); - const merchantResp = await ws.http.get(tipStatusUrl.href); - const tipPickupStatus = await readSuccessResponseJsonOrThrow( - merchantResp, - codecForTipPickupGetResponse(), - ); - logger.trace(`status ${j2s(tipPickupStatus)}`); - - const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount); - - logger.trace("new tip, creating tip record"); - await updateExchangeFromUrl(ws, tipPickupStatus.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, - amount, - undefined, - ); - - const walletTipId = encodeCrock(getRandomBytes(32)); - await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url); - const denoms = await getCandidateWithdrawalDenoms( - ws, - tipPickupStatus.exchange_url, - ); - const selectedDenoms = selectWithdrawalDenominations(amount, denoms); - - const secretSeed = encodeCrock(getRandomBytes(64)); - const denomSelUid = encodeCrock(getRandomBytes(32)); - - const newTipRecord: TipRecord = { - walletTipId: walletTipId, - acceptedTimestamp: undefined, - status: TipRecordStatus.DialogAccept, - tipAmountRaw: Amounts.stringify(amount), - tipExpiration: tipPickupStatus.expiration, - exchangeBaseUrl: tipPickupStatus.exchange_url, - next_url: tipPickupStatus.next_url, - merchantBaseUrl: res.merchantBaseUrl, - createdTimestamp: TalerPreciseTimestamp.now(), - merchantTipId: res.merchantTipId, - tipAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue), - denomsSel: selectedDenoms, - pickedUpTimestamp: undefined, - secretSeed, - denomSelUid, - }; - await ws.db - .mktx((x) => [x.tips]) - .runReadWrite(async (tx) => { - await tx.tips.put(newTipRecord); - }); - tipRecord = newTipRecord; - } - - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Tip, - walletTipId: tipRecord.walletTipId, - }); - - const tipStatus: PrepareTipResult = { - accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, - tipAmountRaw: Amounts.stringify(tipRecord.tipAmountRaw), - exchangeBaseUrl: tipRecord.exchangeBaseUrl, - merchantBaseUrl: tipRecord.merchantBaseUrl, - expirationTimestamp: tipRecord.tipExpiration, - tipAmountEffective: Amounts.stringify(tipRecord.tipAmountEffective), - walletTipId: tipRecord.walletTipId, - transactionId, - }; - - return tipStatus; -} - -export async function processTip( - ws: InternalWalletState, - walletTipId: string, -): Promise<TaskRunResult> { - const tipRecord = await ws.db - .mktx((x) => [x.tips]) - .runReadOnly(async (tx) => { - return tx.tips.get(walletTipId); - }); - if (!tipRecord) { - return TaskRunResult.finished(); - } - - switch (tipRecord.status) { - case TipRecordStatus.Aborted: - case TipRecordStatus.DialogAccept: - case TipRecordStatus.Done: - case TipRecordStatus.SuspendidPickup: - return TaskRunResult.finished(); - } - - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Tip, - walletTipId, - }); - - const denomsForWithdraw = tipRecord.denomsSel; - - const planchets: DerivedTipPlanchet[] = []; - // Planchets in the form that the merchant expects - const planchetsDetail: TipPlanchetDetail[] = []; - const denomForPlanchet: { [index: number]: DenominationRecord } = []; - - for (const dh of denomsForWithdraw.selectedDenoms) { - const denom = await ws.db - .mktx((x) => [x.denominations]) - .runReadOnly(async (tx) => { - return tx.denominations.get([ - tipRecord.exchangeBaseUrl, - dh.denomPubHash, - ]); - }); - checkDbInvariant(!!denom, "denomination should be in database"); - for (let i = 0; i < dh.count; i++) { - const deriveReq = { - denomPub: denom.denomPub, - planchetIndex: planchets.length, - secretSeed: tipRecord.secretSeed, - }; - logger.trace(`deriving tip planchet: ${j2s(deriveReq)}`); - const p = await ws.cryptoApi.createTipPlanchet(deriveReq); - logger.trace(`derive result: ${j2s(p)}`); - denomForPlanchet[planchets.length] = denom; - planchets.push(p); - planchetsDetail.push({ - coin_ev: p.coinEv, - denom_pub_hash: denom.denomPubHash, - }); - } - } - - const tipStatusUrl = new URL( - `tips/${tipRecord.merchantTipId}/pickup`, - tipRecord.merchantBaseUrl, - ); - - const req = { planchets: planchetsDetail }; - logger.trace(`sending tip request: ${j2s(req)}`); - const merchantResp = await ws.http.postJson(tipStatusUrl.href, req); - - logger.trace(`got tip response, status ${merchantResp.status}`); - - // FIXME: Why do we do this? - if ( - (merchantResp.status >= 500 && merchantResp.status <= 599) || - merchantResp.status === 424 - ) { - logger.trace(`got transient tip error`); - // FIXME: wrap in another error code that indicates a transient error - return { - type: TaskRunResultType.Error, - errorDetail: makeErrorDetail( - TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - getHttpResponseErrorDetails(merchantResp), - "tip pickup failed (transient)", - ), - }; - } - let blindedSigs: BlindedDenominationSignature[] = []; - - const response = await readSuccessResponseJsonOrThrow( - merchantResp, - codecForMerchantTipResponseV2(), - ); - blindedSigs = response.blind_sigs.map((x) => x.blind_sig); - - if (blindedSigs.length !== planchets.length) { - throw Error("number of tip responses does not match requested planchets"); - } - - const newCoinRecords: CoinRecord[] = []; - - for (let i = 0; i < blindedSigs.length; i++) { - const blindedSig = blindedSigs[i]; - - const denom = denomForPlanchet[i]; - checkLogicInvariant(!!denom); - const planchet = planchets[i]; - checkLogicInvariant(!!planchet); - - if (denom.denomPub.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher"); - } - - if (blindedSig.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher"); - } - - const denomSigRsa = await ws.cryptoApi.rsaUnblind({ - bk: planchet.blindingKey, - blindedSig: blindedSig.blinded_rsa_signature, - pk: denom.denomPub.rsa_public_key, - }); - - const isValid = await ws.cryptoApi.rsaVerify({ - hm: planchet.coinPub, - pk: denom.denomPub.rsa_public_key, - sig: denomSigRsa.sig, - }); - - if (!isValid) { - return { - type: TaskRunResultType.Error, - errorDetail: makeErrorDetail( - TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID, - {}, - "invalid signature from the exchange (via merchant tip) after unblinding", - ), - }; - } - - newCoinRecords.push({ - blindingKey: planchet.blindingKey, - coinPriv: planchet.coinPriv, - coinPub: planchet.coinPub, - coinSource: { - type: CoinSourceType.Tip, - coinIndex: i, - walletTipId: walletTipId, - }, - sourceTransactionId: transactionId, - denomPubHash: denom.denomPubHash, - denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig }, - exchangeBaseUrl: tipRecord.exchangeBaseUrl, - status: CoinStatus.Fresh, - coinEvHash: planchet.coinEvHash, - maxAge: AgeRestriction.AGE_UNRESTRICTED, - ageCommitmentProof: planchet.ageCommitmentProof, - spendAllocation: undefined, - }); - } - - const transitionInfo = await ws.db - .mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips]) - .runReadWrite(async (tx) => { - const tr = await tx.tips.get(walletTipId); - if (!tr) { - return; - } - if (tr.status !== TipRecordStatus.PendingPickup) { - return; - } - const oldTxState = computeTipTransactionStatus(tr); - tr.pickedUpTimestamp = TalerPreciseTimestamp.now(); - tr.status = TipRecordStatus.Done; - await tx.tips.put(tr); - const newTxState = computeTipTransactionStatus(tr); - for (const cr of newCoinRecords) { - await makeCoinAvailable(ws, tx, cr); - } - await makeCoinsVisible(ws, tx, transactionId); - return { oldTxState, newTxState }; - }); - notifyTransition(ws, transactionId, transitionInfo); - ws.notify({ type: NotificationType.BalanceChange }); - - return TaskRunResult.finished(); -} - -export async function acceptTip( - ws: InternalWalletState, - walletTipId: string, -): Promise<AcceptTipResponse> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Tip, - walletTipId, - }); - const dbRes = await ws.db - .mktx((x) => [x.tips]) - .runReadWrite(async (tx) => { - const tipRecord = await tx.tips.get(walletTipId); - if (!tipRecord) { - logger.error("tip not found"); - return; - } - if (tipRecord.status != TipRecordStatus.DialogAccept) { - logger.warn("Unable to accept tip in the current state"); - return { tipRecord }; - } - const oldTxState = computeTipTransactionStatus(tipRecord); - tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now(); - tipRecord.status = TipRecordStatus.PendingPickup; - await tx.tips.put(tipRecord); - const newTxState = computeTipTransactionStatus(tipRecord); - return { tipRecord, transitionInfo: { oldTxState, newTxState } }; - }); - - if (!dbRes) { - throw Error("tip not found"); - } - - notifyTransition(ws, transactionId, dbRes.transitionInfo); - - const tipRecord = dbRes.tipRecord; - - return { - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Tip, - walletTipId: walletTipId, - }), - next_url: tipRecord.next_url, - }; -} - -export async function suspendTipTransaction( - ws: InternalWalletState, - walletTipId: string, -): Promise<void> { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.TipPickup, - walletTipId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Tip, - walletTipId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.tips]) - .runReadWrite(async (tx) => { - const tipRec = await tx.tips.get(walletTipId); - if (!tipRec) { - logger.warn(`transaction tip ${walletTipId} not found`); - return; - } - let newStatus: TipRecordStatus | undefined = undefined; - switch (tipRec.status) { - case TipRecordStatus.Done: - case TipRecordStatus.SuspendidPickup: - case TipRecordStatus.Aborted: - case TipRecordStatus.DialogAccept: - break; - case TipRecordStatus.PendingPickup: - newStatus = TipRecordStatus.SuspendidPickup; - break; - - default: - assertUnreachable(tipRec.status); - } - if (newStatus != null) { - const oldTxState = computeTipTransactionStatus(tipRec); - tipRec.status = newStatus; - const newTxState = computeTipTransactionStatus(tipRec); - await tx.tips.put(tipRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumeTipTransaction( - ws: InternalWalletState, - walletTipId: string, -): Promise<void> { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.TipPickup, - walletTipId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Tip, - walletTipId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.tips]) - .runReadWrite(async (tx) => { - const tipRec = await tx.tips.get(walletTipId); - if (!tipRec) { - logger.warn(`transaction tip ${walletTipId} not found`); - return; - } - let newStatus: TipRecordStatus | undefined = undefined; - switch (tipRec.status) { - case TipRecordStatus.Done: - case TipRecordStatus.PendingPickup: - case TipRecordStatus.Aborted: - case TipRecordStatus.DialogAccept: - break; - case TipRecordStatus.SuspendidPickup: - newStatus = TipRecordStatus.PendingPickup; - break; - default: - assertUnreachable(tipRec.status); - } - if (newStatus != null) { - const oldTxState = computeTipTransactionStatus(tipRec); - tipRec.status = newStatus; - const newTxState = computeTipTransactionStatus(tipRec); - await tx.tips.put(tipRec); - 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, - walletTipId: string, -): Promise<void> { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.TipPickup, - walletTipId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Tip, - walletTipId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.tips]) - .runReadWrite(async (tx) => { - const tipRec = await tx.tips.get(walletTipId); - if (!tipRec) { - logger.warn(`transaction tip ${walletTipId} not found`); - return; - } - let newStatus: TipRecordStatus | undefined = undefined; - switch (tipRec.status) { - case TipRecordStatus.Done: - case TipRecordStatus.Aborted: - case TipRecordStatus.PendingPickup: - case TipRecordStatus.DialogAccept: - break; - case TipRecordStatus.SuspendidPickup: - newStatus = TipRecordStatus.Aborted; - break; - default: - assertUnreachable(tipRec.status); - } - if (newStatus != null) { - const oldTxState = computeTipTransactionStatus(tipRec); - tipRec.status = newStatus; - const newTxState = computeTipTransactionStatus(tipRec); - await tx.tips.put(tipRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} |