commit 0f2ab3431cb55f35d37d77ee5ba84e1cdabffa57
parent c88e885096bb5bcb67e76b1bd99f07f71adf166e
Author: Florian Dold <dold@taler.net>
Date: Fri, 4 Sep 2026 01:02:16 +0200
wallet-core: persist Donau statement totals
Store statement totals, include them in balances, and reject receipts
with invalid donation unit keys.
Diffstat:
3 files changed, 77 insertions(+), 11 deletions(-)
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
@@ -524,8 +524,7 @@ class BalancesStore {
donauBaseUrl: rec.donauBaseUrl,
year: rec.year,
legalDomain: rec.legalDomain,
- // Not yet implemented
- amountStatement: undefined,
+ amountStatement: rec.amountStatement,
});
}
diff --git a/packages/taler-wallet-core/src/donau.test.ts b/packages/taler-wallet-core/src/donau.test.ts
@@ -23,10 +23,29 @@ import assert from "node:assert";
import { test } from "node:test";
import {
CandidateDonationUnit,
+ buildDonauStatementUri,
generateDonauPlanchets,
selectDonationUnitsExact,
} from "./donau.js";
+test("Donau statement URIs preserve the service base path", () => {
+ assert.strictEqual(
+ buildDonauStatementUri(
+ {
+ donauBaseUrl: "https://donau.example/service/instance/",
+ year: 2026,
+ donorTaxId: "tax/id",
+ donorHashSalt: "salt value",
+ },
+ {
+ total: "EUR:12.5",
+ donation_statement_sig: "SIGNATURE",
+ },
+ ),
+ "donau://donau.example/service/instance/2026/tax%2Fid/salt%20value?total=EUR%3A12.5&sig=ED25519:SIGNATURE",
+ );
+});
+
function candidate(value: string): CandidateDonationUnit {
return {
value: `EUR:${value}`,
@@ -68,6 +87,7 @@ test("Donau planchet generation does not log taxpayer or blinding secrets", asyn
const body = {
base_url: "https://donau.example/",
currency: "EUR",
+ legal_domain: "donau.example",
donation_units: [
{
donation_unit_pub: {
diff --git a/packages/taler-wallet-core/src/donau.ts b/packages/taler-wallet-core/src/donau.ts
@@ -46,6 +46,7 @@ import {
GetDonauStatementsResponse,
getRandomBytes,
HashCodeString,
+ HttpStatusCode,
j2s,
Logger,
NotificationType,
@@ -203,28 +204,74 @@ async function fetchDonauStatements(
});
const conf = succeedOrThrow(await donauClient.getConfig());
- const stmt = succeedOrThrow(
- await donauClient.getDonationStatement(group.year, group.donorTaxIdHash),
+ const statementResult = await donauClient.getDonationStatement(
+ group.year,
+ group.donorTaxIdHash,
);
- const parsedDonauUrl = new URL(group.donauBaseUrl);
- const proto = parsedDonauUrl.protocol == "http:" ? "donau+http" : "donau";
- const taxIdEnc = encodeURIComponent(group.donorTaxId);
- const saltEnc = encodeURIComponent(group.donorHashSalt);
+ if (statementResult.case === HttpStatusCode.NoContent) {
+ await persistDonauStatementTotal(wex, group, undefined);
+ continue;
+ }
+ const stmt = succeedOrThrow(statementResult);
+ await persistDonauStatementTotal(wex, group, stmt.total);
statements.push({
donationStatementSig: stmt.donation_statement_sig,
donauPub: stmt.donau_pub,
total: stmt.total,
year: group.year,
legalDomain: conf.legal_domain,
- // FIXME: Generate this using some helper
- // FIXME: What about a donau not at the root path?
- uri: `${proto}://${parsedDonauUrl.host}/${group.year}/${taxIdEnc}/${saltEnc}?total=${stmt.total}&sig=ED25519:${stmt.donation_statement_sig}`,
+ uri: buildDonauStatementUri(group, stmt),
});
}
return statements;
}
+async function persistDonauStatementTotal(
+ wex: WalletExecutionContext,
+ group: DonationReceiptGroup,
+ total: AmountString | undefined,
+): Promise<void> {
+ await wex.runWalletDbTx(async (tx) => {
+ const summary = await tx.getDonationSummary(
+ group.donauBaseUrl,
+ group.year,
+ group.currency,
+ );
+ if (!summary) {
+ return;
+ }
+ summary.amountStatement = total;
+ await tx.upsertDonationSummary(summary);
+ tx.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: "donau",
+ });
+ });
+}
+
+export function buildDonauStatementUri(
+ group: Pick<
+ DonationReceiptGroup,
+ "donauBaseUrl" | "year" | "donorTaxId" | "donorHashSalt"
+ >,
+ stmt: {
+ total: AmountString;
+ donation_statement_sig: string;
+ },
+): string {
+ const base = new URL(group.donauBaseUrl);
+ const proto = base.protocol === "http:" ? "donau+http" : "donau";
+ const prefix = base.pathname.endsWith("/")
+ ? base.pathname
+ : `${base.pathname}/`;
+ return `${proto}://${base.host}${prefix}${group.year}/${encodeURIComponent(
+ group.donorTaxId,
+ )}/${encodeURIComponent(group.donorHashSalt)}?total=${encodeURIComponent(
+ stmt.total,
+ )}&sig=ED25519:${stmt.donation_statement_sig}`;
+}
+
function groupDonauReceipts(
receipts: WalletDonationReceipt[],
): DonationReceiptGroup[] {