taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-util/src/types-taler-wallet.ts | 11+++++++++++
Mpackages/taler-wallet-core/src/balance.ts | 1+
Mpackages/taler-wallet-core/src/db.ts | 13++++++++++++-
Mpackages/taler-wallet-core/src/donau.ts | 263+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 3++-
Mpackages/taler-wallet-core/src/wallet.ts | 3++-
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]: {