commit ccfef3a5f43cb3f9f7124fd0c51321aa0c4bfd20
parent 3ea4b76323d3afb4d04ea9c8a0ddb34e586addfe
Author: Florian Dold <florian@dold.me>
Date: Tue, 21 Oct 2025 16:36:43 +0200
wallet-core: integrate donau summaries into balances
Diffstat:
6 files changed, 143 insertions(+), 16 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-donau.ts b/packages/taler-harness/src/integrationtests/test-donau.ts
@@ -239,6 +239,12 @@ export async function runDonauTest(t: GlobalTestState) {
t.assertDeepEqual(orderStatus.order_status, "paid");
}
+ const transactions = await walletClient.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ console.log(j2s(transactions.donauSummary));
+
const statements = await walletClient.call(
WalletApiOperation.GetDonauStatements,
{},
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -537,8 +537,27 @@ export function stringifyScopeInfo(si: ScopeInfo): string {
}
}
+export interface DonauSummaryItem {
+ /** Base URL of the donau service. */
+ donauBaseUrl: string;
+ /** Year of the donation(s). */
+ year: number;
+ /**
+ * Sum of donation receipts in the
+ * applicable year.
+ */
+ amountReceiptsAvailable: AmountString;
+ amountReceiptsSubmitted: AmountString;
+ /**
+ * Amount of the latest available statement. Missing if no statement
+ * was requested yet.
+ */
+ amountStatement?: AmountString;
+}
+
export interface BalancesResponse {
balances: WalletBalance[];
+ donauSummary?: DonauSummaryItem[];
}
export const codecForBalance = (): Codec<WalletBalance> =>
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
@@ -59,6 +59,7 @@ import {
BalanceFlag,
BalancesResponse,
checkDbInvariant,
+ DonauSummaryItem,
GetBalanceDetailRequest,
j2s,
Logger,
@@ -69,6 +70,7 @@ import {
import { ExchangeRestrictionSpec, findMatchingWire } from "./coinSelection.js";
import {
DepositOperationStatus,
+ DonationSummaryRecord,
ExchangeEntryDbRecordStatus,
OPERATION_STATUS_NONFINAL_FIRST,
OPERATION_STATUS_NONFINAL_LAST,
@@ -157,6 +159,7 @@ function getScopeSortingOrder(scopeInfo: ScopeInfo): number {
class BalancesStore {
private exchangeScopeCache: Record<string, ScopeInfo> = {};
private balanceStore: Record<string, WalletBalance> = {};
+ private donauSummaryItems: DonauSummaryItem[] | undefined = undefined;
constructor(
private wex: WalletExecutionContext,
@@ -260,11 +263,30 @@ class BalancesStore {
b.flagOutgoingKyc = true;
}
+ addDonauItem(rec: DonationSummaryRecord): void {
+ if (!this.donauSummaryItems) {
+ this.donauSummaryItems = [];
+ }
+
+ this.donauSummaryItems.push({
+ amountReceiptsAvailable: rec.amountReceiptsAvailable,
+ amountReceiptsSubmitted: rec.amountReceiptsSubmitted,
+ donauBaseUrl: rec.donauBaseUrl,
+ year: rec.year,
+ // Not yet implemented
+ amountStatement: undefined,
+ });
+ }
+
toBalancesResponse(): BalancesResponse {
const balancesResponse: BalancesResponse = {
balances: [],
};
+ if (this.donauSummaryItems) {
+ balancesResponse.donauSummary = this.donauSummaryItems;
+ }
+
const balanceStore = this.balanceStore;
Object.keys(balanceStore)
@@ -325,6 +347,7 @@ export async function getBalancesInsideTransaction(
"peerPullDebit",
"purchases",
"coins",
+ "donationSummaries",
]
>,
): Promise<BalancesResponse> {
@@ -335,6 +358,10 @@ export async function getBalancesInsideTransaction(
OPERATION_STATUS_NONFINAL_LAST,
);
+ await tx.donationSummaries.iter().forEachAsync(async (rec) => {
+ balanceStore.addDonauItem(rec);
+ });
+
await tx.exchanges.iter().forEachAsync(async (ex) => {
if (
ex.entryStatus === ExchangeEntryDbRecordStatus.Used ||
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -72,7 +72,6 @@ import {
WireInfo,
WithdrawalExchangeAccountDetails,
ZeroLimitedOperation,
- bytesToString,
canonicalJson,
codecForAny,
encodeCrock,
@@ -170,7 +169,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
* backwards-compatible way or object stores and indices
* are added.
*/
-export const WALLET_DB_MINOR_VERSION = 21;
+export const WALLET_DB_MINOR_VERSION = 22;
declare const symDbProtocolTimestamp: unique symbol;
@@ -1096,7 +1095,7 @@ export interface TokenRecord extends TokenFamilyInfo {
* Blinding secret for token.
*/
blindingKey: string;
-};
+}
/**
* Slate, a blank slice of rock cut for use as a writing surface,
@@ -1115,9 +1114,7 @@ export namespace TokenRecord {
extraData: r.extraData,
tokenIssuePub: r.tokenIssuePub,
};
- return encodeCrock(
- hash(stringToBytes(canonicalJson(info) + "\0")),
- );
+ return encodeCrock(hash(stringToBytes(canonicalJson(info) + "\0")));
}
}
@@ -2899,6 +2896,14 @@ export interface DonationReceiptRecord {
udiIndex: number;
}
+export interface DonationSummaryRecord {
+ donauBaseUrl: string;
+ year: number;
+ currency: string;
+ amountReceiptsAvailable: AmountString;
+ amountReceiptsSubmitted: AmountString;
+}
+
/**
* Schema definition for the IndexedDB
* wallet database.
@@ -3058,13 +3063,9 @@ export const WalletStoresV1 = {
versionAdded: 17,
},
),
- byTokenFamilyHash: describeIndex(
- "byTokenFamilyHash",
- "tokenFamilyHash",
- {
- versionAdded: 21,
- },
- ),
+ byTokenFamilyHash: describeIndex("byTokenFamilyHash", "tokenFamilyHash", {
+ versionAdded: 21,
+ }),
},
),
slates: describeStore(
@@ -3233,6 +3234,13 @@ export const WalletStoresV1 = {
}),
},
}),
+ donationSummaries: describeStoreV2({
+ recordCodec: passthroughCodec<DonationSummaryRecord>(),
+ storeName: "donationSummaries",
+ keyPath: ["donauBaseUrl", "year", "currency"],
+ versionAdded: 22,
+ indexes: {},
+ }),
withdrawalGroups: describeStore(
"withdrawalGroups",
describeContents<WithdrawalGroupRecord>({
@@ -3862,7 +3870,7 @@ async function fixupTokenFamilyHash(
): Promise<void> {
const tokens = await tx.tokens.getAll();
for (const token of tokens) {
- if (! token.tokenFamilyHash) {
+ if (!token.tokenFamilyHash) {
logger.info(`hashing token family info for ${token.tokenIssuePubHash}`);
token.tokenFamilyHash = TokenRecord.hashInfo(token);
await tx.tokens.put(token);
diff --git a/packages/taler-wallet-core/src/donau.ts b/packages/taler-wallet-core/src/donau.ts
@@ -46,6 +46,7 @@ import {
HashCodeString,
j2s,
Logger,
+ NotificationType,
SetDonauRequest,
SignedTokenEnvelope,
stringToBytes,
@@ -114,6 +115,9 @@ export async function handleGetDonauStatements(
if (!r0) {
continue;
}
+ const year = r0.donationYear;
+ const donauBaseUrl = r0.donauBaseUrl;
+ const currency = Amounts.currencyOf(r0.value);
succeedOrThrow(
await donauClient.submitDonationReceipts({
h_donor_tax_id: r0.donorTaxIdHash,
@@ -122,10 +126,46 @@ export async function handleGetDonauStatements(
h_donation_unit_pub: x.donationUnitPubHash,
nonce: x.udiNonce,
})),
- donation_year: r0.donationYear,
+ donation_year: year,
}),
);
+ await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
+ let donauSummary = await tx.donationSummaries.get([
+ donauBaseUrl,
+ year,
+ currency,
+ ]);
+ if (!donauSummary) {
+ logger.warn(
+ `no donau summary (your database might be an old development version, upgrad not supported)`,
+ );
+ return;
+ }
+ for (const bi of batch) {
+ const receipt = await tx.donationReceipts.get(bi.udiNonce);
+ if (!receipt) {
+ continue;
+ }
+ switch (receipt.status) {
+ case DonationReceiptStatus.Pending: {
+ receipt.status = DonationReceiptStatus.DoneSubmitted;
+ await tx.donationReceipts.put(receipt);
+ donauSummary.amountReceiptsSubmitted = Amounts.stringify(
+ Amounts.add(donauSummary.amountReceiptsSubmitted, receipt.value)
+ .amount,
+ );
+ break;
+ }
+ }
+ }
+ await tx.donationSummaries.put(donauSummary);
+ tx.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: "donau",
+ });
+ });
+
const stmt = succeedOrThrow(
await donauClient.getDonationStatement(
r0.donationYear,
@@ -197,7 +237,7 @@ export async function handleSetDonau(
}
/**
- * Implementation of the setDonau
+ * Implementation of the getDonau
* wallet-core request.
*/
export async function handleGetDonau(
@@ -517,6 +557,27 @@ export async function acceptDonauBlindSigs(
if (existingReceipt) {
continue;
}
+ const year = myPlanchet.donationYear;
+ const currency = Amounts.currencyOf(myPlanchet.value);
+ let donauSummary = await tx.donationSummaries.get([
+ donauBaseUrl,
+ year,
+ Amounts.currencyOf(myPlanchet.value),
+ ]);
+ const zero = Amounts.stringify(Amounts.zeroOfCurrency(currency));
+ if (!donauSummary) {
+ donauSummary = {
+ year,
+ amountReceiptsAvailable: zero,
+ amountReceiptsSubmitted: zero,
+ currency,
+ donauBaseUrl,
+ };
+ }
+ donauSummary.amountReceiptsAvailable = Amounts.stringify(
+ Amounts.add(donauSummary.amountReceiptsAvailable, myPlanchet.value)
+ .amount,
+ );
await tx.donationReceipts.put({
donationUnitSig: sigs[i],
donationUnitPubHash: myPlanchet.donationUnitPubHash,
@@ -531,6 +592,11 @@ export async function acceptDonauBlindSigs(
udiIndex: myPlanchet.udiIndex,
value: myPlanchet.value,
});
+ await tx.donationSummaries.put(donauSummary);
+ tx.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: "donau",
+ });
}
});
}
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -444,6 +444,7 @@ export type GetBalancesOp = {
request: EmptyObject;
response: BalancesResponse;
};
+
export type GetBalancesDetailOp = {
op: WalletApiOperation.GetBalanceDetail;
request: GetBalanceDetailRequest;