summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-03-31 16:50:34 +0200
committerFlorian Dold <florian@dold.me>2024-03-31 16:50:34 +0200
commitf45340eb11435f47a3a561c724cd356e5b4ba885 (patch)
tree3c2a079d78202908f0007fe065f7225ba016e415 /packages/taler-wallet-core
parent2d61180dce798ab260d47f94b382fd4f843a55bf (diff)
downloadwallet-core-f45340eb11435f47a3a561c724cd356e5b4ba885.tar.gz
wallet-core-f45340eb11435f47a3a561c724cd356e5b4ba885.tar.bz2
wallet-core-f45340eb11435f47a3a561c724cd356e5b4ba885.zip
wallet-core: implement denom-loss transaction
Diffstat (limited to 'packages/taler-wallet-core')
-rw-r--r--packages/taler-wallet-core/src/db.ts38
-rw-r--r--packages/taler-wallet-core/src/dev-experiments.ts24
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts279
-rw-r--r--packages/taler-wallet-core/src/recoup.ts36
-rw-r--r--packages/taler-wallet-core/src/shepherd.ts18
-rw-r--r--packages/taler-wallet-core/src/transactions.ts98
-rw-r--r--packages/taler-wallet-core/src/versions.ts6
-rw-r--r--packages/taler-wallet-core/src/wallet.ts4
8 files changed, 416 insertions, 87 deletions
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,