diff options
author | Florian Dold <florian@dold.me> | 2024-03-31 16:50:34 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-03-31 16:50:34 +0200 |
commit | f45340eb11435f47a3a561c724cd356e5b4ba885 (patch) | |
tree | 3c2a079d78202908f0007fe065f7225ba016e415 /packages | |
parent | 2d61180dce798ab260d47f94b382fd4f843a55bf (diff) | |
download | wallet-core-f45340eb11435f47a3a561c724cd356e5b4ba885.tar.xz |
wallet-core: implement denom-loss transaction
Diffstat (limited to 'packages')
-rw-r--r-- | packages/taler-harness/src/integrationtests/test-denom-unoffered.ts | 9 | ||||
-rw-r--r-- | packages/taler-util/src/notifications.ts | 1 | ||||
-rw-r--r-- | packages/taler-util/src/transactions-types.ts | 20 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 38 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/dev-experiments.ts | 24 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/exchanges.ts | 279 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/recoup.ts | 36 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/shepherd.ts | 18 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/transactions.ts | 98 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/versions.ts | 6 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 4 |
11 files changed, 442 insertions, 91 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts index 79269d533..677739627 100644 --- a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts +++ b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts @@ -21,6 +21,7 @@ import { MerchantApiClient, PreparePayResultType, TalerErrorCode, + TransactionType, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; @@ -93,7 +94,7 @@ export async function runDenomUnofferedTest(t: GlobalTestState) { ); const confirmResp = await walletClient.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, + transactionId: preparePayResult.transactionId, }); const tx = await walletClient.call(WalletApiOperation.GetTransactionById, { @@ -147,8 +148,12 @@ export async function runDenomUnofferedTest(t: GlobalTestState) { await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const txs = await walletClient.call(WalletApiOperation.GetTransactions, {}); + const txs = await walletClient.call(WalletApiOperation.GetTransactions, { + sort: "stable-ascending", + }); console.log(JSON.stringify(txs, undefined, 2)); + + t.assertDeepEqual(txs.transactions[2].type, TransactionType.DenomLoss); } runDenomUnofferedTest.suites = ["wallet"]; diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts index f439b4a6f..43ca6271e 100644 --- a/packages/taler-util/src/notifications.ts +++ b/packages/taler-util/src/notifications.ts @@ -22,7 +22,6 @@ /** * Imports. */ -import { CancellationToken } from "./CancellationToken.js"; import { AbsoluteTime } from "./time.js"; import { TransactionState } from "./transactions-types.js"; import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js"; diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index 8c4c2c7ed..bddc03c25 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -222,7 +222,8 @@ export type Transaction = | TransactionPeerPushCredit | TransactionPeerPushDebit | TransactionInternalWithdrawal - | TransactionRecoup; + | TransactionRecoup + | TransactionDenomLoss; export enum TransactionType { Withdrawal = "withdrawal", @@ -237,6 +238,7 @@ export enum TransactionType { PeerPullDebit = "peer-pull-debit", PeerPullCredit = "peer-pull-credit", Recoup = "recoup", + DenomLoss = "denom-loss", } export enum WithdrawalType { @@ -298,6 +300,22 @@ interface WithdrawalDetailsForTalerBankIntegrationApi { exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[]; } +export enum DenomLossEventType { + DenomExpired = "denom-expired", + DenomVanished = "denom-vanished", + DenomUnoffered = "denom-unoffered", +} + +/** + * A transaction to indicate financial loss due to denominations + * that became unusable for deposits. + */ +export interface TransactionDenomLoss extends TransactionCommon { + type: TransactionType.DenomLoss; + lossEventType: DenomLossEventType; + exchangeBaseUrl: string; +} + /** * A withdrawal transaction (either bank-integrated or manual). */ diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 98390805b..4ead3cf5c 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -39,6 +39,7 @@ import { CoinPublicKeyString, CoinRefreshRequest, CoinStatus, + DenomLossEventType, DenomSelectionState, DenominationInfo, DenominationPubKey, @@ -149,7 +150,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 8; +export const WALLET_DB_MINOR_VERSION = 9; declare const symDbProtocolTimestamp: unique symbol; @@ -2355,11 +2356,46 @@ export interface TransactionRecord { currency: string; } +export enum DenomLossStatus { + /** + * Done indicates that the loss happened. + */ + Done = 0x0500_0000, + + /** + * Aborted in the sense that the loss was reversed. + */ + Aborted = 0x0503_0001, +} + +export interface DenomLossEventRecord { + denomLossEventId: string; + currency: string; + denomPubHashes: string[]; + status: DenomLossStatus; + timestampCreated: DbPreciseTimestamp; + amount: string; + eventType: DenomLossEventType; + exchangeBaseUrl: string; +} + /** * Schema definition for the IndexedDB * wallet database. */ export const WalletStoresV1 = { + denomLossEvents: describeStoreV2({ + recordCodec: passthroughCodec<DenomLossEventRecord>(), + storeName: "denomLossEvents", + keyPath: "denomLossEventId", + versionAdded: 9, + indexes: { + byCurrency: describeIndex("byCurrency", "currency", { + versionAdded: 9, + }), + byStatus: describeIndex("byStatus", "status"), + }, + }), transactions: describeStoreV2({ recordCodec: passthroughCodec<TransactionRecord>(), storeName: "transactions", diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts index c94571ff8..130c042b6 100644 --- a/packages/taler-wallet-core/src/dev-experiments.ts +++ b/packages/taler-wallet-core/src/dev-experiments.ts @@ -26,6 +26,7 @@ */ import { + DenomLossEventType, Logger, RefreshReason, TalerPreciseTimestamp, @@ -39,6 +40,8 @@ import { HttpResponse, } from "@gnu-taler/taler-util/http"; import { + DenomLossEventRecord, + DenomLossStatus, RefreshGroupRecord, RefreshOperationStatus, timestampPreciseToDb, @@ -88,6 +91,27 @@ export async function applyDevExperiment( return; } + if (parsedUri.devExperimentId == "insert-denom-loss") { + await wex.db.runReadWriteTx(["denomLossEvents"], async (tx) => { + const eventId = encodeCrock(getRandomBytes(32)); + const newRg: DenomLossEventRecord = { + amount: "TESTKUDOS:42", + currency: "TESTKUDOS", + exchangeBaseUrl: "https://exchange.devexperiment.taler.net/", + denomLossEventId: eventId, + denomPubHashes: [ + encodeCrock(getRandomBytes(64)), + encodeCrock(getRandomBytes(64)), + ], + eventType: DenomLossEventType.DenomExpired, + status: DenomLossStatus.Done, + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), + }; + await tx.denomLossEvents.put(newRg); + }); + return; + } + throw Error(`dev-experiment id not understood ${parsedUri.devExperimentId}`); } diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts index 8b4bca2aa..a57215ee4 100644 --- a/packages/taler-wallet-core/src/exchanges.ts +++ b/packages/taler-wallet-core/src/exchanges.ts @@ -26,6 +26,7 @@ import { AbsoluteTime, AgeRestriction, + Amount, Amounts, AsyncFlag, CancellationToken, @@ -33,6 +34,7 @@ import { CoinStatus, DeleteExchangeRequest, DenomKeyType, + DenomLossEventType, DenomOperationMap, DenominationInfo, DenominationPubKey, @@ -65,6 +67,10 @@ import { TalerPreciseTimestamp, TalerProtocolDuration, TalerProtocolTimestamp, + TransactionIdStr, + TransactionMajorState, + TransactionState, + TransactionType, URL, WalletNotification, WireFee, @@ -77,6 +83,7 @@ import { codecForExchangeKeysJson, durationMul, encodeCrock, + getRandomBytes, hashDenomPub, j2s, makeErrorDetail, @@ -90,9 +97,11 @@ import { } from "@gnu-taler/taler-util/http"; import { PendingTaskType, + TaskIdStr, TaskIdentifiers, TaskRunResult, TaskRunResultType, + TransactionContext, constructTaskIdentifier, getAutoRefreshExecuteThreshold, getExchangeEntryStatusFromRecord, @@ -101,6 +110,8 @@ import { getExchangeUpdateStatusFromRecord, } from "./common.js"; import { + DenomLossEventRecord, + DenomLossStatus, DenominationRecord, DenominationVerificationStatus, ExchangeDetailsRecord, @@ -126,6 +137,7 @@ import { import { DbReadOnlyTransaction } from "./query.js"; import { createRecoupGroup } from "./recoup.js"; import { createRefreshGroup } from "./refresh.js"; +import { constructTransactionIdentifier } from "./transactions.js"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js"; import { InternalWalletState, WalletExecutionContext } from "./wallet.js"; @@ -1390,8 +1402,6 @@ export async function updateExchangeFromUrlHandler( ["text/plain"], ); - let recoupGroupId: string | undefined; - logger.trace("updating exchange info in database"); let detailsPointerChanged = false; @@ -1406,11 +1416,11 @@ export async function updateExchangeFromUrlHandler( break; } } - - const now = AbsoluteTime.now(); let noFees = checkNoFees(keysInfo); let peerPaymentsDisabled = checkPeerPaymentsDisabled(keysInfo); + let denomLossResult: boolean = false; + const updated = await wex.db.runReadWriteTx( [ "exchanges", @@ -1420,6 +1430,8 @@ export async function updateExchangeFromUrlHandler( "coins", "refreshGroups", "recoupGroups", + "coinAvailability", + "denomLossEvents", ], async (tx) => { const r = await tx.exchanges.get(exchangeBaseUrl); @@ -1508,6 +1520,7 @@ export async function updateExchangeFromUrlHandler( const currentDenomSet = new Set<string>( keysInfo.currentDenominations.map((x) => x.denomPubHash), ); + for (const currentDenom of keysInfo.currentDenominations) { const oldDenom = oldDenomByDph.get(currentDenom.denomPubHash); if (oldDenom) { @@ -1552,44 +1565,14 @@ export async function updateExchangeFromUrlHandler( logger.trace("done updating denominations in database"); - // Handle recoup - const recoupDenomList = keysInfo.recoup; - const newlyRevokedCoinPubs: string[] = []; - logger.trace("recoup list from exchange", recoupDenomList); - for (const recoupInfo of recoupDenomList) { - const oldDenom = await tx.denominations.get([ - r.baseUrl, - recoupInfo.h_denom_pub, - ]); - if (!oldDenom) { - // We never even knew about the revoked denomination, all good. - continue; - } - if (oldDenom.isRevoked) { - // We already marked the denomination as revoked, - // this implies we revoked all coins - logger.trace("denom already revoked"); - continue; - } - logger.info("revoking denom", recoupInfo.h_denom_pub); - oldDenom.isRevoked = true; - await tx.denominations.put(oldDenom); - const affectedCoins = await tx.coins.indexes.byDenomPubHash - .iter(recoupInfo.h_denom_pub) - .toArray(); - for (const ac of affectedCoins) { - newlyRevokedCoinPubs.push(ac.coinPub); - } - } - if (newlyRevokedCoinPubs.length != 0) { - logger.info("recouping coins", newlyRevokedCoinPubs); - recoupGroupId = await createRecoupGroup( - wex, - tx, - exchangeBaseUrl, - newlyRevokedCoinPubs, - ); - } + denomLossResult = await handleDenomLoss( + wex, + tx, + newDetails.currency, + exchangeBaseUrl, + ); + + await handleRecoup(wex, tx, exchangeBaseUrl, keysInfo.recoup); const newExchangeState = getExchangeState(r); @@ -1602,20 +1585,17 @@ export async function updateExchangeFromUrlHandler( }, ); - if (recoupGroupId) { - const recoupTaskId = constructTaskIdentifier({ - tag: PendingTaskType.Recoup, - recoupGroupId, - }); - // Asynchronously start recoup. This doesn't need to finish - // for the exchange update to be considered finished. - wex.taskScheduler.startShepherdTask(recoupTaskId); - } - if (!updated) { throw Error("something went wrong with updating the exchange"); } + if (denomLossResult) { + wex.ws.notify({ + type: NotificationType.BalanceChange, + hintTransactionId: "denom-loss:*", + }); + } + logger.trace("done updating exchange info in database"); logger.trace(`doing auto-refresh check for '${exchangeBaseUrl}'`); @@ -1709,6 +1689,201 @@ export async function updateExchangeFromUrlHandler( return TaskRunResult.progress(); } +async function handleDenomLoss( + wex: WalletExecutionContext, + tx: WalletDbReadWriteTransaction< + ["coinAvailability", "denominations", "denomLossEvents"] + >, + currency: string, + exchangeBaseUrl: string, +): Promise<boolean> { + const coinAvailabilityRecs = + await tx.coinAvailability.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl); + const denomsVanished: string[] = []; + const denomsUnoffered: string[] = []; + const denomsExpired: string[] = []; + let amountVanished = Amount.zeroOfCurrency(currency); + let amountExpired = Amount.zeroOfCurrency(currency); + let amountUnoffered = Amount.zeroOfCurrency(currency); + + for (const coinAv of coinAvailabilityRecs) { + if (coinAv.freshCoinCount <= 0) { + continue; + } + const n = coinAv.freshCoinCount; + const denom = await tx.denominations.get(coinAv.denomPubHash); + if (!denom) { + // Remove availability + coinAv.freshCoinCount = 0; + coinAv.visibleCoinCount = 0; + await tx.coinAvailability.put(coinAv); + denomsVanished.push(coinAv.denomPubHash); + const total = Amount.from(coinAv.value).mult(n); + amountVanished = amountVanished.add(total); + continue; + } + if (!denom.isOffered) { + // Remove availability + coinAv.freshCoinCount = 0; + coinAv.visibleCoinCount = 0; + await tx.coinAvailability.put(coinAv); + denomsUnoffered.push(coinAv.denomPubHash); + const total = Amount.from(coinAv.value).mult(n); + amountUnoffered = amountUnoffered.add(total); + continue; + } + const timestampExpireDeposit = timestampAbsoluteFromDb( + denom.stampExpireDeposit, + ); + if (AbsoluteTime.isExpired(timestampExpireDeposit)) { + // Remove availability + coinAv.freshCoinCount = 0; + coinAv.visibleCoinCount = 0; + await tx.coinAvailability.put(coinAv); + denomsExpired.push(coinAv.denomPubHash); + const total = Amount.from(coinAv.value).mult(n); + amountExpired = amountExpired.add(total); + continue; + } + } + let hadLoss = false; + + if (denomsVanished.length > 0) { + hadLoss = true; + await tx.denomLossEvents.add({ + denomLossEventId: encodeCrock(getRandomBytes(32)), + amount: amountVanished.toString(), + currency, + exchangeBaseUrl, + denomPubHashes: denomsVanished, + eventType: DenomLossEventType.DenomVanished, + status: DenomLossStatus.Done, + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), + }); + } + + if (denomsUnoffered.length > 0) { + hadLoss = true; + await tx.denomLossEvents.add({ + denomLossEventId: encodeCrock(getRandomBytes(32)), + amount: amountUnoffered.toString(), + currency, + exchangeBaseUrl, + denomPubHashes: denomsUnoffered, + eventType: DenomLossEventType.DenomUnoffered, + status: DenomLossStatus.Done, + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), + }); + } + + if (denomsExpired.length > 0) { + hadLoss = true; + await tx.denomLossEvents.add({ + denomLossEventId: encodeCrock(getRandomBytes(32)), + amount: amountExpired.toString(), + currency, + exchangeBaseUrl, + denomPubHashes: denomsUnoffered, + eventType: DenomLossEventType.DenomExpired, + status: DenomLossStatus.Done, + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), + }); + } + + return hadLoss; +} + +export function computeDenomLossTransactionStatus( + rec: DenomLossEventRecord, +): TransactionState { + switch (rec.status) { + case DenomLossStatus.Aborted: + return { + major: TransactionMajorState.Aborted, + }; + case DenomLossStatus.Done: + return { + major: TransactionMajorState.Done, + }; + } +} + +export class DenomLossTransactionContext implements TransactionContext { + get taskId(): TaskIdStr | undefined { + return undefined; + } + transactionId: TransactionIdStr; + + abortTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + suspendTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + resumeTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + failTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + deleteTransaction(): Promise<void> { + throw new Error("Method not implemented."); + } + + constructor( + wex: WalletExecutionContext, + public denomLossEventId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.DenomLoss, + denomLossEventId, + }); + } +} + +async function handleRecoup( + wex: WalletExecutionContext, + tx: WalletDbReadWriteTransaction< + ["denominations", "coins", "recoupGroups", "refreshGroups"] + >, + exchangeBaseUrl: string, + recoup: Recoup[], +): Promise<void> { + // Handle recoup + const recoupDenomList = recoup; + const newlyRevokedCoinPubs: string[] = []; + logger.trace("recoup list from exchange", recoupDenomList); + for (const recoupInfo of recoupDenomList) { + const oldDenom = await tx.denominations.get([ + exchangeBaseUrl, + recoupInfo.h_denom_pub, + ]); + if (!oldDenom) { + // We never even knew about the revoked denomination, all good. + continue; + } + if (oldDenom.isRevoked) { + // We already marked the denomination as revoked, + // this implies we revoked all coins + logger.trace("denom already revoked"); + continue; + } + logger.info("revoking denom", recoupInfo.h_denom_pub); + oldDenom.isRevoked = true; + await tx.denominations.put(oldDenom); + const affectedCoins = await tx.coins.indexes.byDenomPubHash.getAll( + recoupInfo.h_denom_pub, + ); + for (const ac of affectedCoins) { + newlyRevokedCoinPubs.push(ac.coinPub); + } + } + if (newlyRevokedCoinPubs.length != 0) { + logger.info("recouping coins", newlyRevokedCoinPubs); + await createRecoupGroup(wex, tx, exchangeBaseUrl, newlyRevokedCoinPubs); + } +} + function getAutoRefreshExecuteThresholdForDenom( d: DenominationRecord, ): AbsoluteTime { diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts index 8d5d3dd1f..b8b2cf808 100644 --- a/packages/taler-wallet-core/src/recoup.ts +++ b/packages/taler-wallet-core/src/recoup.ts @@ -26,7 +26,6 @@ */ import { Amounts, - CancellationToken, CoinStatus, Logger, RefreshReason, @@ -63,11 +62,7 @@ import { } from "./db.js"; import { createRefreshGroup } from "./refresh.js"; import { constructTransactionIdentifier } from "./transactions.js"; -import { - WalletExecutionContext, - getDenomInfo, - type InternalWalletState, -} from "./wallet.js"; +import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; import { internalCreateWithdrawalGroup } from "./withdraw.js"; export const logger = new Logger("operations/recoup.ts"); @@ -237,15 +232,18 @@ export async function recoupWithdrawCoin( cs: WithdrawCoinSource, ): Promise<void> { const reservePub = cs.reservePub; - const denomInfo = await wex.db.runReadOnlyTx(["denominations"], async (tx) => { - const denomInfo = await getDenomInfo( - wex, - tx, - coin.exchangeBaseUrl, - coin.denomPubHash, - ); - return denomInfo; - }); + const denomInfo = await wex.db.runReadOnlyTx( + ["denominations"], + async (tx) => { + const denomInfo = await getDenomInfo( + wex, + tx, + coin.exchangeBaseUrl, + coin.denomPubHash, + ); + return denomInfo; + }, + ); if (!denomInfo) { // FIXME: We should at least emit some pending operation / warning for this? return; @@ -420,7 +418,7 @@ export async function processRecoupGroup( return TaskRunResult.finished(); } -export class RewardTransactionContext implements TransactionContext { +export class RecoupTransactionContext implements TransactionContext { abortTransaction(): Promise<void> { throw new Error("Method not implemented."); } @@ -440,7 +438,7 @@ export class RewardTransactionContext implements TransactionContext { public taskId: TaskIdStr; constructor( - public ws: InternalWalletState, + public wex: WalletExecutionContext, private recoupGroupId: string, ) { this.transactionId = constructTransactionIdentifier({ @@ -487,6 +485,10 @@ export async function createRecoupGroup( await tx.recoupGroups.put(recoupGroup); + const ctx = new RecoupTransactionContext(wex, recoupGroupId); + + wex.taskScheduler.startShepherdTask(ctx.taskId); + return recoupGroupId; } diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts index f04bcd2c2..4ca472e7b 100644 --- a/packages/taler-wallet-core/src/shepherd.ts +++ b/packages/taler-wallet-core/src/shepherd.ts @@ -61,7 +61,10 @@ import { computeDepositTransactionStatus, processDepositGroup, } from "./deposits.js"; -import { updateExchangeFromUrlHandler } from "./exchanges.js"; +import { + computeDenomLossTransactionStatus, + updateExchangeFromUrlHandler, +} from "./exchanges.js"; import { computePayMerchantTransactionState, computeRefundTransactionState, @@ -636,6 +639,7 @@ async function getTransactionState( "peerPushCredit", "rewards", "refreshGroups", + "denomLossEvents", ] >, transactionId: string, @@ -674,12 +678,13 @@ async function getTransactionState( } return computeRefundTransactionState(rec); } - case TransactionType.PeerPullCredit: + case TransactionType.PeerPullCredit: { const rec = await tx.peerPullCredit.get(parsedTxId.pursePub); if (!rec) { return undefined; } return computePeerPullCreditTransactionState(rec); + } case TransactionType.PeerPullDebit: { const rec = await tx.peerPullDebit.get(parsedTxId.peerPullDebitId); if (!rec) { @@ -717,6 +722,13 @@ async function getTransactionState( } case TransactionType.Recoup: throw Error("not yet supported"); + case TransactionType.DenomLoss: { + const rec = await tx.denomLossEvents.get(parsedTxId.denomLossEventId); + if (!rec) { + return undefined; + } + return computeDenomLossTransactionStatus(rec); + } default: assertUnreachable(parsedTxId); } @@ -864,6 +876,8 @@ export function listTaskForTransactionId(transactionId: string): TaskIdStr[] { withdrawalGroupId: tid.withdrawalGroupId, }), ]; + case TransactionType.DenomLoss: + return []; default: assertUnreachable(tid); } diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts index 7ece43dc5..e404c0354 100644 --- a/packages/taler-wallet-core/src/transactions.ts +++ b/packages/taler-wallet-core/src/transactions.ts @@ -23,7 +23,6 @@ import { Amounts, assertUnreachable, checkDbInvariant, - checkLogicInvariant, DepositTransactionTrackingState, j2s, Logger, @@ -38,6 +37,7 @@ import { TalerErrorCode, TalerPreciseTimestamp, Transaction, + TransactionAction, TransactionByIdRequest, TransactionIdStr, TransactionMajorState, @@ -59,6 +59,7 @@ import { TransactionContext, } from "./common.js"; import { + DenomLossEventRecord, DepositElementStatus, DepositGroupRecord, OPERATION_STATUS_ACTIVE_FIRST, @@ -76,7 +77,6 @@ import { RefreshGroupRecord, RefreshOperationStatus, RefundGroupRecord, - RewardRecord, timestampPreciseFromDb, timestampProtocolFromDb, WalletDbReadOnlyTransaction, @@ -90,6 +90,8 @@ import { DepositTransactionContext, } from "./deposits.js"; import { + computeDenomLossTransactionStatus, + DenomLossTransactionContext, ExchangeWireDetails, getExchangeWireDetailsInTx, } from "./exchanges.js"; @@ -127,11 +129,7 @@ import { computeRefreshTransactionState, RefreshTransactionContext, } from "./refresh.js"; -import { - computeRewardTransactionStatus, - computeTipTransactionActions, - RewardTransactionContext, -} from "./reward.js"; +import { RewardTransactionContext } from "./reward.js"; import type { WalletExecutionContext } from "./wallet.js"; import { augmentPaytoUrisForWithdrawal, @@ -212,6 +210,7 @@ const txOrder: { [t in TransactionType]: number } = { [TransactionType.Refresh]: 10, [TransactionType.Recoup]: 11, [TransactionType.InternalWithdrawal]: 12, + [TransactionType.DenomLoss]: 13, }; export async function getTransactionById( @@ -268,6 +267,19 @@ export async function getTransactionById( ); } + case TransactionType.DenomLoss: { + const rec = await wex.db.runReadOnlyTx( + ["denomLossEvents"], + async (tx) => { + return tx.denomLossEvents.get(parsedTx.denomLossEventId); + }, + ); + if (!rec) { + throw Error("denom loss record not found"); + } + return buildTransactionForDenomLoss(rec); + } + case TransactionType.Recoup: throw new Error("not yet supported"); @@ -859,6 +871,24 @@ function buildTransactionForRefresh( }; } +function buildTransactionForDenomLoss(rec: DenomLossEventRecord): Transaction { + const txState = computeDenomLossTransactionStatus(rec); + return { + type: TransactionType.DenomLoss, + txState, + txActions: [TransactionAction.Delete], + amountRaw: Amounts.stringify(rec.amount), + amountEffective: Amounts.stringify(rec.amount), + timestamp: timestampPreciseFromDb(rec.timestampCreated), + transactionId: constructTransactionIdentifier({ + tag: TransactionType.DenomLoss, + denomLossEventId: rec.denomLossEventId, + }), + lossEventType: rec.eventType, + exchangeBaseUrl: rec.exchangeBaseUrl, + }; +} + function buildTransactionForDeposit( dg: DepositGroupRecord, ort?: OperationRetryRecord, @@ -1079,6 +1109,7 @@ export async function getTransactions( "withdrawalGroups", "refreshGroups", "refundGroups", + "denomLossEvents", ], async (tx) => { await iterRecordsForPeerPushDebit(tx, filter, async (pi) => { @@ -1325,6 +1356,21 @@ export async function getTransactions( } }); + await iterRecordsForDenomLoss(tx, filter, async (rec) => { + const amount = Amounts.parseOrThrow(rec.amount); + const exchangesInTx = [rec.exchangeBaseUrl]; + if ( + shouldSkipCurrency( + transactionsRequest, + amount.currency, + exchangesInTx, + ) + ) { + return; + } + transactions.push(buildTransactionForDenomLoss(rec)); + }); + await iterRecordsForDeposit(tx, filter, async (dg) => { const amount = Amounts.parseOrThrow(dg.amount); const exchangesInTx = dg.infoPerExchange @@ -1476,7 +1522,8 @@ export type ParsedTransactionIdentifier = | { tag: TransactionType.Reward; walletRewardId: string } | { tag: TransactionType.Withdrawal; withdrawalGroupId: string } | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string } - | { tag: TransactionType.Recoup; recoupGroupId: string }; + | { tag: TransactionType.Recoup; recoupGroupId: string } + | { tag: TransactionType.DenomLoss; denomLossEventId: string }; export function constructTransactionIdentifier( pTxId: ParsedTransactionIdentifier, @@ -1506,6 +1553,8 @@ export function constructTransactionIdentifier( return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr; case TransactionType.Recoup: return `txn:${pTxId.tag}:${pTxId.recoupGroupId}` as TransactionIdStr; + case TransactionType.DenomLoss: + return `txn:${pTxId.tag}:${pTxId.denomLossEventId}` as TransactionIdStr; default: assertUnreachable(pTxId); } @@ -1565,6 +1614,11 @@ export function parseTransactionIdentifier( tag: TransactionType.Withdrawal, withdrawalGroupId: rest[0], }; + case TransactionType.DenomLoss: + return { + tag: TransactionType.DenomLoss, + denomLossEventId: rest[0], + }; default: return undefined; } @@ -1636,6 +1690,9 @@ function maybeTaskFromTransaction( tag: PendingTaskType.Recoup, recoupGroupId: parsedTx.recoupGroupId, }); + case TransactionType.DenomLoss: + // Nothing to do for denom loss + return undefined; default: assertUnreachable(parsedTx); } @@ -1687,8 +1744,10 @@ async function getContextForTransaction( case TransactionType.Reward: return new RewardTransactionContext(wex, tx.walletRewardId); case TransactionType.Recoup: + //return new RecoupTransactionContext(ws, tx.recoupGroupId); throw new Error("not yet supported"); - //return new RecoupTransactionContext(ws, tx.recoupGroupId); + case TransactionType.DenomLoss: + return new DenomLossTransactionContext(wex, tx.denomLossEventId); default: assertUnreachable(tx); } @@ -1847,6 +1906,27 @@ async function iterRecordsForDeposit( } } +async function iterRecordsForDenomLoss( + tx: WalletDbReadOnlyTransaction<["denomLossEvents"]>, + filter: TransactionRecordFilter, + f: (r: DenomLossEventRecord) => Promise<void>, +): Promise<void> { + let dgs: DenomLossEventRecord[]; + if (filter.onlyState === "nonfinal") { + const keyRange = GlobalIDB.KeyRange.bound( + OPERATION_STATUS_ACTIVE_FIRST, + OPERATION_STATUS_ACTIVE_LAST, + ); + dgs = await tx.denomLossEvents.indexes.byStatus.getAll(keyRange); + } else { + dgs = await tx.denomLossEvents.indexes.byStatus.getAll(); + } + + for (const dg of dgs) { + await f(dg); + } +} + async function iterRecordsForRefund( tx: WalletDbReadOnlyTransaction<["refundGroups"]>, filter: TransactionRecordFilter, diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts index bf8a9f7c8..ad58a66ec 100644 --- a/packages/taler-wallet-core/src/versions.ts +++ b/packages/taler-wallet-core/src/versions.ts @@ -50,11 +50,9 @@ export const WALLET_COREBANK_API_PROTOCOL_VERSION = "2:0:0"; export const WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION = "2:0:0"; /** - * Semver of the wallet-core API implementation. - * Will be replaced with the value from package.json in a - * post-compilation step (inside lib/). + * Libtool version of the wallet-core API. */ -export const WALLET_CORE_API_IMPLEMENTATION_VERSION = "3:0:2"; +export const WALLET_CORE_API_PROTOCOL_VERSION = "4:0:0"; /** * Libtool rules: diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index c203f6648..7cc5ab93b 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -274,7 +274,7 @@ import { WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION, WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_COREBANK_API_PROTOCOL_VERSION, - WALLET_CORE_API_IMPLEMENTATION_VERSION, + WALLET_CORE_API_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_MERCHANT_PROTOCOL_VERSION, } from "./versions.js"; @@ -1448,7 +1448,7 @@ export function getVersion(wex: WalletExecutionContext): WalletCoreVersion { implementationSemver: walletCoreBuildInfo.implementationSemver, implementationGitHash: walletCoreBuildInfo.implementationGitHash, hash: undefined, - version: WALLET_CORE_API_IMPLEMENTATION_VERSION, + version: WALLET_CORE_API_PROTOCOL_VERSION, exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, merchant: WALLET_MERCHANT_PROTOCOL_VERSION, bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION, |