taler-typescript-core

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

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:
Mpackages/taler-harness/src/integrationtests/test-donau.ts | 6++++++
Mpackages/taler-util/src/types-taler-wallet.ts | 19+++++++++++++++++++
Mpackages/taler-wallet-core/src/balance.ts | 27+++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db.ts | 36++++++++++++++++++++++--------------
Mpackages/taler-wallet-core/src/donau.ts | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 1+
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;