commit 412659f638bf2e6905b6e64c67ad4cbcdf2ebd89
parent d1b04a2b731e67da43cf5aa8dbc13c9fafb8d2c6
Author: Iván Ávalos <avalos@disroot.org>
Date: Wed, 22 Oct 2025 16:13:22 +0200
wallet-core: refactor logic and add donauBaseUrl field to getDonauStatements request
Diffstat:
6 files changed, 204 insertions(+), 90 deletions(-)
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -549,6 +549,8 @@ export function stringifyScopeInfo(si: ScopeInfo): string {
export interface DonauSummaryItem {
/** Base URL of the donau service. */
donauBaseUrl: string;
+ /** Legal domain of the donau service (if available). */
+ legalDomain?: string;
/** Year of the donation(s). */
year: number;
/**
@@ -4375,6 +4377,10 @@ export interface DonauStatementItem {
donauPub: EddsaPublicKeyString;
}
+export interface GetDonauStatementsRequest {
+ donauBaseUrl?: string;
+}
+
export interface GetDonauStatementsResponse {
statements: DonauStatementItem[];
}
@@ -4387,3 +4393,8 @@ export interface GetDonauResponse {
}
| undefined;
}
+
+export const codecForGetDonauStatementsRequest = (): Codec<GetDonauStatementsRequest> =>
+ buildCodecForObject<GetDonauStatementsRequest>()
+ .property("donauBaseUrl", codecOptional(codecForString()))
+ .build("GetDonauStatementsRequest");
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
@@ -273,6 +273,7 @@ class BalancesStore {
amountReceiptsSubmitted: rec.amountReceiptsSubmitted,
donauBaseUrl: rec.donauBaseUrl,
year: rec.year,
+ legalDomain: rec.legalDomain,
// Not yet implemented
amountStatement: undefined,
});
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -169,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 = 22;
+export const WALLET_DB_MINOR_VERSION = 23;
declare const symDbProtocolTimestamp: unique symbol;
@@ -2898,6 +2898,7 @@ export interface DonationReceiptRecord {
export interface DonationSummaryRecord {
donauBaseUrl: string;
+ legalDomain?: string;
year: number;
currency: string;
amountReceiptsAvailable: AmountString;
@@ -3232,6 +3233,16 @@ export const WalletStoresV1 = {
byStatus: describeIndex("byStatus", "status", {
versionAdded: 20,
}),
+ byDonauBaseUrl: describeIndex("byDonauBaseUrl", "donauBaseUrl", {
+ versionAdded: 23,
+ }),
+ byStatusAndDonauBaseUrl: describeIndex(
+ "byStatusAndDonauBaseUrl",
+ ["status", "donauBaseUrl"],
+ {
+ versionAdded: 23,
+ },
+ ),
},
}),
donationSummaries: describeStoreV2({
diff --git a/packages/taler-wallet-core/src/donau.ts b/packages/taler-wallet-core/src/donau.ts
@@ -41,6 +41,7 @@ import {
EmptyObject,
encodeCrock,
GetDonauResponse,
+ GetDonauStatementsRequest,
GetDonauStatementsResponse,
getRandomBytes,
HashCodeString,
@@ -66,37 +67,181 @@ import { WalletExecutionContext } from "./index.js";
*/
const logger = new Logger("donau.ts");
+type DonationReceiptGroup = {
+ receipts: DonationReceiptRecord[];
+ donauBaseUrl: string;
+ donorTaxId: string;
+ donorTaxIdHash: string;
+ donorHashSalt: string;
+ year: number;
+ currency: string;
+};
+
/**
* Implementation of the getDonauStatements
* wallet-core request.
*/
export async function handleGetDonauStatements(
wex: WalletExecutionContext,
- _req: EmptyObject,
+ req: GetDonauStatementsRequest,
): Promise<GetDonauStatementsResponse> {
- const statements: DonauStatementItem[] = [];
- const pendingReceipts = await wex.db.runAllStoresReadOnlyTx(
- {},
+ await submitDonationReceipts(wex, req.donauBaseUrl);
+ return {
+ statements: await fetchDonauStatements(wex, req.donauBaseUrl),
+ };
+}
+
+async function submitDonationReceipts(
+ wex: WalletExecutionContext,
+ donauBaseUrl?: string,
+): Promise<void> {
+ const receipts = await wex.db.runReadOnlyTx(
+ {
+ storeNames: ["donationReceipts"],
+ },
async (tx) => {
- return await tx.donationReceipts.indexes.byStatus.getAll(
- DonationReceiptStatus.Pending,
- );
+ let receipts;
+ if (donauBaseUrl) {
+ receipts = await tx.donationReceipts.indexes.byStatusAndDonauBaseUrl.getAll([
+ DonationReceiptStatus.Pending,
+ donauBaseUrl,
+ ]);
+ } else {
+ receipts = await tx.donationReceipts.indexes.byStatus.getAll(
+ DonationReceiptStatus.Pending,
+ );
+ }
+ return receipts;
},
);
- logger.info(`have ${pendingReceipts.length} pending donation receipts`);
+ const groups = groupDonauReceipts(receipts);
+ for (const group of groups) {
+ const donauClient = new DonauHttpClient(group.donauBaseUrl);
+ const conf = succeedOrThrow(await donauClient.getConfig());
+
+ logger.info(`submitting donation receipts (${group.year}, ${group.donorTaxIdHash})`);
+ succeedOrThrow(
+ await donauClient.submitDonationReceipts({
+ h_donor_tax_id: group.donorTaxIdHash,
+ donation_receipts: group.receipts.map((x) => ({
+ donation_unit_sig: x.donationUnitSig,
+ h_donation_unit_pub: x.donationUnitPubHash,
+ nonce: x.udiNonce,
+ })),
+ donation_year: group.year,
+ }),
+ );
+
+ await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
+ let donauSummary = await tx.donationSummaries.get([
+ group.donauBaseUrl,
+ group.year,
+ group.currency,
+ ]);
+ if (!donauSummary) {
+ logger.warn(
+ `no donau summary (your database might be an old development version, upgrade not supported)`,
+ );
+ return;
+ }
+
+ donauSummary.legalDomain = conf.legal_domain;
+
+ for (const bi of group.receipts) {
+ 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",
+ });
+ });
+ }
+}
+async function fetchDonauStatements(
+ wex: WalletExecutionContext,
+ donauBaseUrl?: string,
+): Promise<DonauStatementItem[]> {
+ const receipts = await wex.db.runReadOnlyTx(
+ {
+ storeNames: ["donationReceipts"],
+ },
+ async (tx) => {
+ let receipts;
+ if (donauBaseUrl) {
+ receipts = await tx.donationReceipts.indexes.byStatusAndDonauBaseUrl.getAll([
+ DonationReceiptStatus.DoneSubmitted,
+ donauBaseUrl,
+ ]);
+ } else {
+ receipts = await tx.donationReceipts.indexes.byStatus.getAll(
+ DonationReceiptStatus.DoneSubmitted,
+ );
+ }
+ return receipts;
+ },
+ );
+
+ const statements: DonauStatementItem[] = [];
+ const groups = groupDonauReceipts(receipts);
+ for (const group of groups) {
+ const donauClient = new DonauHttpClient(group.donauBaseUrl);
+ const conf = succeedOrThrow(await donauClient.getConfig());
+
+ const stmt = succeedOrThrow(
+ 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);
+ 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}`,
+ });
+ }
+
+ return statements;
+}
+
+function groupDonauReceipts(
+ receipts: DonationReceiptRecord[],
+): DonationReceiptGroup[] {
const donauUrlSet = new Set<string>(
- pendingReceipts.map((x) => x.donauBaseUrl),
+ receipts.map((x) => x.donauBaseUrl),
);
const donauUrls = [...donauUrlSet];
+ const groups: DonationReceiptGroup[] = [];
for (const donauUrl of donauUrls) {
- const donauClient = new DonauHttpClient(donauUrl);
- const conf = succeedOrThrow(await donauClient.getConfig());
- // Map from `${taxIdHash}-${year}` to receipts (in the same hash/year/baseUrl)
const buckets: Map<string, DonationReceiptRecord[]> = new Map();
- for (const receipt of pendingReceipts) {
+ for (const receipt of receipts) {
if (receipt.donauBaseUrl != donauUrl) {
continue;
}
@@ -109,89 +254,29 @@ export async function handleGetDonauStatements(
}
bucket.push(receipt);
}
- for (const batch of buckets.values()) {
- logger.info(`submitting donation receipt`);
- const r0 = batch[0];
+
+ for (const [key, value] of buckets) {
+ const r0 = value[0];
if (!r0) {
continue;
}
+
+ groups.push({
+ receipts: value,
+ donauBaseUrl: donauUrl,
+ donorTaxId: r0.donorTaxId,
+ donorTaxIdHash: r0.donorTaxIdHash,
+ donorHashSalt: r0.donorHashSalt,
+ year: r0.donationYear,
+ currency: Amounts.currencyOf(r0.value),
+ })
const year = r0.donationYear;
const donauBaseUrl = r0.donauBaseUrl;
const currency = Amounts.currencyOf(r0.value);
- succeedOrThrow(
- await donauClient.submitDonationReceipts({
- h_donor_tax_id: r0.donorTaxIdHash,
- donation_receipts: batch.map((x) => ({
- donation_unit_sig: x.donationUnitSig,
- h_donation_unit_pub: x.donationUnitPubHash,
- nonce: x.udiNonce,
- })),
- 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,
- r0.donorTaxIdHash,
- ),
- );
- const parsedDonauUrl = new URL(r0.donauBaseUrl);
- const proto = parsedDonauUrl.protocol == "http:" ? "donau+http" : "donau";
- const taxIdEnc = encodeURIComponent(r0.donorTaxId);
- const saltEnc = encodeURIComponent(r0.donorHashSalt);
- statements.push({
- donationStatementSig: stmt.donation_statement_sig,
- donauPub: stmt.donau_pub,
- total: stmt.total,
- year: r0.donationYear,
- legalDomain: conf.legal_domain,
- // FIXME: Generate this using some helper
- // FIXME: What about a donau not at the root path?
- uri: `${proto}://${parsedDonauUrl.host}/${r0.donationYear}/${taxIdEnc}/${saltEnc}?total=${stmt.total}&sig=ED25519:${stmt.donation_statement_sig}`,
- });
}
}
- return {
- statements,
- };
+ return groups;
}
/**
@@ -495,6 +580,9 @@ export async function acceptDonauBlindSigs(
// FIXME: Take this from the database instead of querying each time.
const keysResp = succeedOrThrow(await client.getKeys());
+ // FIXME: Take this from the database instead of querying each time.
+ const conf = succeedOrThrow(await client.getConfig());
+
const sigs: DonationReceiptSignature[] = [];
for (let i = 0; i < donauBlindedSigs.length; i++) {
@@ -592,6 +680,7 @@ export async function acceptDonauBlindSigs(
udiIndex: myPlanchet.udiIndex,
value: myPlanchet.value,
});
+ donauSummary.legalDomain = conf.legal_domain;
await tx.donationSummaries.put(donauSummary);
tx.notify({
type: NotificationType.BalanceChange,
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -92,6 +92,7 @@ import {
GetDepositWireTypesRequest,
GetDepositWireTypesResponse,
GetDonauResponse,
+ GetDonauStatementsRequest,
GetDonauStatementsResponse,
GetExchangeDetailedInfoRequest,
GetExchangeEntryByUrlRequest,
@@ -430,7 +431,7 @@ export type GetDonauOp = {
*/
export type GetDonauStatementsOp = {
op: WalletApiOperation.GetDonauStatements;
- request: EmptyObject;
+ request: GetDonauStatementsRequest;
response: GetDonauStatementsResponse;
};
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -183,6 +183,7 @@ import {
codecForGetCurrencyInfoRequest,
codecForGetDepositWireTypesForCurrencyRequest,
codecForGetDepositWireTypesRequest,
+ codecForGetDonauStatementsRequest,
codecForGetExchangeEntryByUrlRequest,
codecForGetExchangeResourcesRequest,
codecForGetExchangeTosRequest,
@@ -1892,7 +1893,7 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = {
handler: handleTestingWaitExchangeState,
},
[WalletApiOperation.GetDonauStatements]: {
- codec: codecForEmptyObject(),
+ codec: codecForGetDonauStatementsRequest(),
handler: handleGetDonauStatements,
},
[WalletApiOperation.GetDonau]: {