taler-typescript-core

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

commit 05eba69261fdc8c3a914fe78ed1a8a05b377c6a2
parent 0a4392deabb7c8e90f879e18b8ca82844438f077
Author: Florian Dold <dold@taler.net>
Date:   Sat,  5 Sep 2026 13:44:42 +0200

wallet-core: persist transaction amount accounting

Store confirmed amounts, consumed inputs and settled refresh outputs in
operation records. Preserve these fields in SQLite, IndexedDB and database
conversion, with schema migrations and round-trip coverage.

Issue: https://bugs.taler.net/n/7903

Diffstat:
Mpackages/taler-wallet-core/src/db/indexeddb/schema.ts | 7++++++-
Mpackages/taler-wallet-core/src/db/indexeddb/transaction.ts | 24++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db/records.ts | 23+++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db/sqlite/schema.ts | 17++++++++++++++++-
Mpackages/taler-wallet-core/src/db/sqlite/transaction.ts | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mpackages/taler-wallet-core/src/db/testing/conformance-cases.ts | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 213 insertions(+), 20 deletions(-)

diff --git a/packages/taler-wallet-core/src/db/indexeddb/schema.ts b/packages/taler-wallet-core/src/db/indexeddb/schema.ts @@ -76,6 +76,7 @@ import { WalletToken, WalletTombstone, WalletTransactionMeta, + WalletTransactionAmounts, WalletWithdrawalGroup, } from "../records.js"; export { @@ -167,7 +168,7 @@ export const TALER_WALLET_DB_GENERATION_PREFIX = `${TALER_WALLET_MAIN_DB_NAME}-g * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 34; +export const WALLET_DB_MINOR_VERSION = 35; export interface BankWithdrawUriRecord { /** @@ -190,6 +191,7 @@ export interface DbPeerPushPaymentCoinSelection { * Record for a push P2P payment that this wallet initiated. */ export interface PeerPushDebitRecord { + transactionAmounts?: WalletTransactionAmounts; /** * What exchange are funds coming from? */ @@ -263,6 +265,7 @@ export interface PeerPushDebitRecord { } export interface PeerPullCreditRecord { + transactionAmounts?: WalletTransactionAmounts; /** * What exchange are we using for the payment request? */ @@ -334,6 +337,7 @@ export interface PeerPullCreditRecord { * Unique: (exchangeBaseUrl, pursePub) */ export interface PeerPushCreditRecord { + transactionAmounts?: WalletTransactionAmounts; peerPushCreditId: string; exchangeBaseUrl: string; @@ -409,6 +413,7 @@ export interface PeerPullPaymentCoinSelection { * AKA PeerPullDebit. */ export interface PeerPullPaymentIncomingRecord { + transactionAmounts?: WalletTransactionAmounts; peerPullDebitId: string; pursePub: string; diff --git a/packages/taler-wallet-core/src/db/indexeddb/transaction.ts b/packages/taler-wallet-core/src/db/indexeddb/transaction.ts @@ -1631,6 +1631,9 @@ export class IdbWalletTransaction implements WalletDbTransaction { return undefined; } return { + ...(r.transactionAmounts === undefined + ? {} + : { transactionAmounts: r.transactionAmounts }), exchangeBaseUrl: r.exchangeBaseUrl, amount: r.amount, estimatedAmountEffective: r.estimatedAmountEffective, @@ -1662,6 +1665,9 @@ export class IdbWalletTransaction implements WalletDbTransaction { async upsertPeerPullCredit(rec: WalletPeerPullCredit): Promise<void> { const tx = this.tx; await tx.peerPullCredit.put({ + ...(rec.transactionAmounts === undefined + ? {} + : { transactionAmounts: rec.transactionAmounts }), exchangeBaseUrl: rec.exchangeBaseUrl, amount: rec.amount, estimatedAmountEffective: rec.estimatedAmountEffective, @@ -1704,6 +1710,9 @@ export class IdbWalletTransaction implements WalletDbTransaction { return undefined; } return { + ...(r.transactionAmounts === undefined + ? {} + : { transactionAmounts: r.transactionAmounts }), exchangeBaseUrl: r.exchangeBaseUrl, restrictScope: r.restrictScope, amount: r.amount, @@ -1729,6 +1738,9 @@ export class IdbWalletTransaction implements WalletDbTransaction { async upsertPeerPushDebit(rec: WalletPeerPushDebit): Promise<void> { const tx = this.tx; await tx.peerPushDebit.put({ + ...(rec.transactionAmounts === undefined + ? {} + : { transactionAmounts: rec.transactionAmounts }), exchangeBaseUrl: rec.exchangeBaseUrl, restrictScope: rec.restrictScope, amount: rec.amount, @@ -1765,6 +1777,9 @@ export class IdbWalletTransaction implements WalletDbTransaction { return undefined; } return { + ...(r.transactionAmounts === undefined + ? {} + : { transactionAmounts: r.transactionAmounts }), peerPushCreditId: r.peerPushCreditId, exchangeBaseUrl: r.exchangeBaseUrl, pursePub: r.pursePub, @@ -1792,6 +1807,9 @@ export class IdbWalletTransaction implements WalletDbTransaction { async upsertPeerPushCredit(rec: WalletPeerPushCredit): Promise<void> { const tx = this.tx; await tx.peerPushCredit.put({ + ...(rec.transactionAmounts === undefined + ? {} + : { transactionAmounts: rec.transactionAmounts }), peerPushCreditId: rec.peerPushCreditId, exchangeBaseUrl: rec.exchangeBaseUrl, pursePub: rec.pursePub, @@ -1845,6 +1863,9 @@ export class IdbWalletTransaction implements WalletDbTransaction { return undefined; } return { + ...(r.transactionAmounts === undefined + ? {} + : { transactionAmounts: r.transactionAmounts }), peerPullDebitId: r.peerPullDebitId, pursePub: r.pursePub, exchangeBaseUrl: r.exchangeBaseUrl, @@ -1865,6 +1886,9 @@ export class IdbWalletTransaction implements WalletDbTransaction { async upsertPeerPullDebit(rec: WalletPeerPullDebit): Promise<void> { const tx = this.tx; await tx.peerPullDebit.put({ + ...(rec.transactionAmounts === undefined + ? {} + : { transactionAmounts: rec.transactionAmounts }), peerPullDebitId: rec.peerPullDebitId, pursePub: rec.pursePub, exchangeBaseUrl: rec.exchangeBaseUrl, diff --git a/packages/taler-wallet-core/src/db/records.ts b/packages/taler-wallet-core/src/db/records.ts @@ -69,6 +69,20 @@ import { export * from "./timestamps.js"; +/** Durable accounting evidence, independent of the transaction metadata view. */ +export interface WalletTransactionAmounts { + parentTransactionId?: string; + isAbortRecovery?: boolean; + initial?: AmountString; + final?: AmountString; + /** Face value of each consumed coin, counted once across reselections. */ + inputs?: Record<string, AmountString>; + missingInputs?: string[]; + unsettledRefreshIds?: string[]; + /** Settled outputs retained when a supporting refresh is deleted. */ + refreshOutputs?: Record<string, AmountString>; +} + /** * Metadata for a transaction. * This object store is effectively a materialzed view of transactions gathered @@ -196,6 +210,7 @@ export interface WalletDepositInfoPerExchange { } export interface WalletDepositGroup { + transactionAmounts?: WalletTransactionAmounts; depositGroupId: string; currency: string; @@ -383,6 +398,7 @@ export interface WalletRefreshGroupPerExchangeInfo { * currency. */ export interface WalletRefreshGroup { + transactionAmounts?: WalletTransactionAmounts; operationStatus: RefreshOperationStatus; /** @@ -605,6 +621,7 @@ export type WgInfo = * the coin selection we want to withdraw. */ export interface WalletWithdrawalGroup { + transactionAmounts?: WalletTransactionAmounts; /** * Unique identifier for the withdrawal group. */ @@ -877,6 +894,7 @@ export interface WalletPurchasePayInfo { * Operation status: {@link purchaseStatus} */ export interface WalletPurchase { + transactionAmounts?: WalletTransactionAmounts; /** * Proposal ID for this purchase. Uniquely identifies the * purchase and the proposal. @@ -1044,6 +1062,7 @@ export interface WalletPurchase { * Metadata about a group of refunds with the merchant. */ export interface WalletRefundGroup { + transactionAmounts?: WalletTransactionAmounts; status: RefundGroupStatus; /** @@ -2416,6 +2435,7 @@ export interface PeerPullPaymentCoinSelection { * Record for a push P2P payment that this wallet initiated. */ export interface WalletPeerPushDebit { + transactionAmounts?: WalletTransactionAmounts; /** * What exchange are funds coming from? */ @@ -2495,6 +2515,7 @@ export interface WalletPeerPushDebit { * Record for a pull P2P payment that this wallet initiated. */ export interface WalletPeerPullCredit { + transactionAmounts?: WalletTransactionAmounts; /** * What exchange are we using for the payment request? */ @@ -2569,6 +2590,7 @@ export interface WalletPeerPullCredit { * Record for a push P2P payment that this wallet was offered. */ export interface WalletPeerPushCredit { + transactionAmounts?: WalletTransactionAmounts; peerPushCreditId: string; exchangeBaseUrl: string; @@ -2635,6 +2657,7 @@ export interface WalletPeerPushCredit { * AKA PeerPullDebit. Record for a pull P2P payment that this wallet was offered. */ export interface WalletPeerPullDebit { + transactionAmounts?: WalletTransactionAmounts; peerPullDebitId: string; pursePub: string; diff --git a/packages/taler-wallet-core/src/db/sqlite/schema.ts b/packages/taler-wallet-core/src/db/sqlite/schema.ts @@ -85,7 +85,7 @@ * * Bump this when adding a migration to {@link schemaMigrations}. */ -export const SQLITE_SCHEMA_VERSION = 20; +export const SQLITE_SCHEMA_VERSION = 21; /** * Tables of the IndexedDB emulation, children before parents. @@ -1463,6 +1463,21 @@ export const schemaMigrations: SchemaMigration[] = [ "ALTER TABLE recoup_groups ADD COLUMN successful_coin_pubs TEXT", ], }, + { + version: 21, + name: "transaction-final-amounts", + statements: [ + "ALTER TABLE withdrawal_groups ADD COLUMN transaction_amounts TEXT", + "ALTER TABLE purchases ADD COLUMN transaction_amounts TEXT", + "ALTER TABLE deposit_groups ADD COLUMN transaction_amounts TEXT", + "ALTER TABLE refresh_groups ADD COLUMN transaction_amounts TEXT", + "ALTER TABLE peer_push_debit ADD COLUMN transaction_amounts TEXT", + "ALTER TABLE peer_push_credit ADD COLUMN transaction_amounts TEXT", + "ALTER TABLE peer_pull_debit ADD COLUMN transaction_amounts TEXT", + "ALTER TABLE peer_pull_credit ADD COLUMN transaction_amounts TEXT", + "ALTER TABLE refund_groups ADD COLUMN transaction_amounts TEXT", + ], + }, ]; /** Native tables that contain wallet records (not schema bookkeeping). */ diff --git a/packages/taler-wallet-core/src/db/sqlite/transaction.ts b/packages/taler-wallet-core/src/db/sqlite/transaction.ts @@ -991,11 +991,12 @@ export class SqliteWalletTransaction implements WalletDbTransaction { async upsertRefundGroup(rec: WalletRefundGroup): Promise<void> { await this.run( - `INSERT INTO refund_groups ( + `INSERT INTO refund_groups (transaction_amounts, refund_group_id, proposal_id, status, timestamp_created, amount_raw, amount_effective, refresh_group_id - ) VALUES ($id, $proposal_id, $status, $ts, $raw, $eff, $refresh_group_id) + ) VALUES ($transactionAmounts, $id, $proposal_id, $status, $ts, $raw, $eff, $refresh_group_id) ON CONFLICT(refund_group_id) DO UPDATE SET + transaction_amounts = excluded.transaction_amounts, proposal_id = excluded.proposal_id, status = excluded.status, timestamp_created = excluded.timestamp_created, @@ -1003,6 +1004,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction { amount_effective = excluded.amount_effective, refresh_group_id = excluded.refresh_group_id`, { + transactionAmounts: + rec.transactionAmounts === undefined + ? null + : jsonToDb(rec.transactionAmounts), id: rec.refundGroupId, proposal_id: rec.proposalId, status: rec.status, @@ -1035,6 +1040,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { // does not exist on WalletRefundGroup survive review. private rowToRefundGroup(row: ResultRow): WalletRefundGroup { return { + ...(row.transaction_amounts == null + ? {} + : { transactionAmounts: dbToJson(row.transaction_amounts) }), refundGroupId: str(row.refund_group_id), proposalId: str(row.proposal_id), status: num(row.status), @@ -2352,6 +2360,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { private rowToWithdrawalGroup(row: ResultRow): WalletWithdrawalGroup { return { + ...(row.transaction_amounts == null + ? {} + : { transactionAmounts: dbToJson(row.transaction_amounts) }), withdrawalGroupId: str(row.withdrawal_group_id), ...(dbToBool(row.legacy) ? { legacy: true } : undefined), wgInfo: this.rowToWgInfo(row), @@ -2437,7 +2448,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction { async upsertWithdrawalGroup(rec: WalletWithdrawalGroup): Promise<void> { const wg = this.wgInfoToCols(rec.wgInfo); await this.run( - `INSERT INTO withdrawal_groups ( + `INSERT INTO withdrawal_groups (transaction_amounts, withdrawal_group_id, withdrawal_type, taler_withdraw_uri, contract_priv, bank_info, exchange_credit_accounts, is_foreign_account, kyc_payto_hash, kyc_access_token, @@ -2448,13 +2459,14 @@ export class SqliteWalletTransaction implements WalletDbTransaction { instructed_amount, reserve_balance_amount, raw_withdrawal_amount, effective_withdrawal_amount, denoms_sel, abort_reason, fail_reason, legacy - ) VALUES ( + ) VALUES ($transactionAmounts, $id, $wtype, $uri, $cpriv, $binfo, $eca, $ifa, $kph, $kat, $klcs, $klcc, $klrg, $klar, $kld, $kwd, $seed, $rpub, $rpriv, $url, $tstart, $tfinish, $status, $age, $ia, $rba, $rwa, $ewa, $ds, $abort, $fail, $legacy ) ON CONFLICT(withdrawal_group_id) DO UPDATE SET + transaction_amounts = excluded.transaction_amounts, withdrawal_type = excluded.withdrawal_type, taler_withdraw_uri = excluded.taler_withdraw_uri, contract_priv = excluded.contract_priv, @@ -2486,6 +2498,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction { fail_reason = excluded.fail_reason, legacy = excluded.legacy`, { + transactionAmounts: + rec.transactionAmounts === undefined + ? null + : jsonToDb(rec.transactionAmounts), id: rec.withdrawalGroupId, ...wg, ifa: boolToDb(rec.isForeignAccount), @@ -2970,6 +2986,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { private rowToPeerPushDebit(row: ResultRow): WalletPeerPushDebit { return { + ...(row.transaction_amounts == null + ? {} + : { transactionAmounts: dbToJson(row.transaction_amounts) }), pursePub: dbToCrock(row.purse_pub), exchangeBaseUrl: str(row.exchange_base_url), amount: dbAmount(row.amount), @@ -3014,18 +3033,19 @@ export class SqliteWalletTransaction implements WalletDbTransaction { async upsertPeerPushDebit(rec: WalletPeerPushDebit): Promise<void> { await this.run( - `INSERT INTO peer_push_debit ( + `INSERT INTO peer_push_debit (transaction_amounts, purse_pub, exchange_base_url, restrict_scope, amount, total_cost, coin_sel, contract_terms_hash, purse_priv, merge_pub, merge_priv, contract_priv, contract_pub, contract_enc_nonce, purse_expiration, timestamp_created, abort_refresh_group_id, abort_reason, fail_reason, status - ) VALUES ( + ) VALUES ($transactionAmounts, $pub, $url, $scope, $amt, $cost, $csel, $cth, $ppriv, $mpub, $mpriv, $cpriv, $cpub, $nonce, $exp, $created, $argi, $abort, $fail, $status ) ON CONFLICT(purse_pub) DO UPDATE SET + transaction_amounts = excluded.transaction_amounts, exchange_base_url = excluded.exchange_base_url, restrict_scope = excluded.restrict_scope, amount = excluded.amount, @@ -3045,6 +3065,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction { fail_reason = excluded.fail_reason, status = excluded.status`, { + transactionAmounts: + rec.transactionAmounts === undefined + ? null + : jsonToDb(rec.transactionAmounts), pub: crockToDb(rec.pursePub), url: rec.exchangeBaseUrl, scope: @@ -3096,6 +3120,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { private rowToPeerPushCredit(row: ResultRow): WalletPeerPushCredit { return { + ...(row.transaction_amounts == null + ? {} + : { transactionAmounts: dbToJson(row.transaction_amounts) }), peerPushCreditId: str(row.peer_push_credit_id), exchangeBaseUrl: str(row.exchange_base_url), pursePub: dbToCrock(row.purse_pub), @@ -3153,19 +3180,20 @@ export class SqliteWalletTransaction implements WalletDbTransaction { async upsertPeerPushCredit(rec: WalletPeerPushCredit): Promise<void> { await this.run( - `INSERT INTO peer_push_credit ( + `INSERT INTO peer_push_credit (transaction_amounts, peer_push_credit_id, exchange_base_url, purse_pub, merge_priv, contract_priv, timestamp, estimated_amount_effective, contract_terms_hash, status, abort_reason, fail_reason, withdrawal_group_id, merge_reserve_row_id, currency, kyc_payto_hash, kyc_access_token, kyc_last_check_status, kyc_last_check_code, kyc_last_rule_gen, kyc_last_aml_review, kyc_last_deny - ) VALUES ( + ) VALUES ($transactionAmounts, $id, $url, $ppub, $mpriv, $cpriv, $ts, $eae, $cth, $status, $abort, $fail, $wgid, $mrri, $cur, $kph, $kat, $klcs, $klcc, $klrg, $klar, $kld ) ON CONFLICT(peer_push_credit_id) DO UPDATE SET + transaction_amounts = excluded.transaction_amounts, exchange_base_url = excluded.exchange_base_url, purse_pub = excluded.purse_pub, merge_priv = excluded.merge_priv, @@ -3187,6 +3215,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction { kyc_last_aml_review = excluded.kyc_last_aml_review, kyc_last_deny = excluded.kyc_last_deny`, { + transactionAmounts: + rec.transactionAmounts === undefined + ? null + : jsonToDb(rec.transactionAmounts), id: rec.peerPushCreditId, url: rec.exchangeBaseUrl, ppub: crockToDb(rec.pursePub), @@ -3252,6 +3284,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { private rowToPeerPullDebit(row: ResultRow): WalletPeerPullDebit { return { + ...(row.transaction_amounts == null + ? {} + : { transactionAmounts: dbToJson(row.transaction_amounts) }), peerPullDebitId: str(row.peer_pull_debit_id), pursePub: dbToCrock(row.purse_pub), exchangeBaseUrl: str(row.exchange_base_url), @@ -3291,16 +3326,17 @@ export class SqliteWalletTransaction implements WalletDbTransaction { async upsertPeerPullDebit(rec: WalletPeerPullDebit): Promise<void> { await this.run( - `INSERT INTO peer_pull_debit ( + `INSERT INTO peer_pull_debit (transaction_amounts, peer_pull_debit_id, purse_pub, exchange_base_url, amount, contract_terms_hash, timestamp_created, contract_priv, status, total_cost_estimated, abort_refresh_group_id, abort_reason, fail_reason, coin_sel, cleanup_final_status - ) VALUES ( + ) VALUES ($transactionAmounts, $id, $ppub, $url, $amt, $cth, $created, $cpriv, $status, $tce, $argi, $abort, $fail, $csel, $cfs ) ON CONFLICT(peer_pull_debit_id) DO UPDATE SET + transaction_amounts = excluded.transaction_amounts, purse_pub = excluded.purse_pub, exchange_base_url = excluded.exchange_base_url, amount = excluded.amount, @@ -3315,6 +3351,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction { coin_sel = excluded.coin_sel, cleanup_final_status = excluded.cleanup_final_status`, { + transactionAmounts: + rec.transactionAmounts === undefined + ? null + : jsonToDb(rec.transactionAmounts), id: rec.peerPullDebitId, ppub: crockToDb(rec.pursePub), url: rec.exchangeBaseUrl, @@ -3361,6 +3401,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { private rowToPeerPullCredit(row: ResultRow): WalletPeerPullCredit { return { + ...(row.transaction_amounts == null + ? {} + : { transactionAmounts: dbToJson(row.transaction_amounts) }), pursePub: dbToCrock(row.purse_pub), exchangeBaseUrl: str(row.exchange_base_url), amount: dbAmount(row.amount), @@ -3421,7 +3464,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction { async upsertPeerPullCredit(rec: WalletPeerPullCredit): Promise<void> { await this.run( - `INSERT INTO peer_pull_credit ( + `INSERT INTO peer_pull_credit (transaction_amounts, purse_pub, exchange_base_url, amount, estimated_amount_effective, purse_priv, contract_terms_hash, merge_pub, merge_priv, contract_pub, contract_priv, contract_enc_nonce, merge_timestamp, @@ -3429,12 +3472,13 @@ export class SqliteWalletTransaction implements WalletDbTransaction { kyc_last_check_status, kyc_last_check_code, kyc_last_rule_gen, kyc_last_aml_review, kyc_last_deny, abort_reason, fail_reason, withdrawal_group_id, purse_create_proof - ) VALUES ( + ) VALUES ($transactionAmounts, $pub, $url, $amt, $eae, $ppriv, $cth, $mpub, $mpriv, $cpub, $cpriv, $nonce, $mts, $mrri, $status, $kph, $kat, $klcs, $klcc, $klrg, $klar, $kld, $abort, $fail, $wgid, $pcp ) ON CONFLICT(purse_pub) DO UPDATE SET + transaction_amounts = excluded.transaction_amounts, exchange_base_url = excluded.exchange_base_url, amount = excluded.amount, estimated_amount_effective = excluded.estimated_amount_effective, @@ -3460,6 +3504,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction { withdrawal_group_id = excluded.withdrawal_group_id, purse_create_proof = excluded.purse_create_proof`, { + transactionAmounts: + rec.transactionAmounts === undefined + ? null + : jsonToDb(rec.transactionAmounts), pub: crockToDb(rec.pursePub), url: rec.exchangeBaseUrl, amt: rec.amount, @@ -3507,6 +3555,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { private rowToDepositGroup(row: ResultRow): WalletDepositGroup { return { + ...(row.transaction_amounts == null + ? {} + : { transactionAmounts: dbToJson(row.transaction_amounts) }), depositGroupId: str(row.deposit_group_id), currency: str(row.currency), amount: dbAmount(row.amount), @@ -3579,7 +3630,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction { async upsertDepositGroup(rec: WalletDepositGroup): Promise<void> { await this.run( - `INSERT INTO deposit_groups ( + `INSERT INTO deposit_groups (transaction_amounts, deposit_group_id, currency, amount, wire_transfer_deadline, merchant_pub, merchant_priv, nonce_priv, nonce_pub, wire, contract_terms_hash, pay_coin_selection, pay_coin_selection_uid, @@ -3589,13 +3640,14 @@ export class SqliteWalletTransaction implements WalletDbTransaction { info_per_exchange, abort_refresh_group_id, abort_reason, fail_reason, kyc_info, kyc_auth_transfer_options, kyc_auth_transfer_expiry, tracking_state - ) VALUES ( + ) VALUES ($transactionAmounts, $id, $cur, $amt, $wtd, $mpub, $mpriv, $npriv, $npub, $wire, $cth, $pcs, $pcsu, $tpc, $ceda, $created, $finished, $lastAttempt, $status, $spc, $ipe, $argi, $abort, $fail, $kyc, $kato, $kate, $tracking ) ON CONFLICT(deposit_group_id) DO UPDATE SET + transaction_amounts = excluded.transaction_amounts, currency = excluded.currency, amount = excluded.amount, wire_transfer_deadline = excluded.wire_transfer_deadline, @@ -3625,6 +3677,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction { kyc_auth_transfer_expiry = excluded.kyc_auth_transfer_expiry, tracking_state = excluded.tracking_state`, { + transactionAmounts: + rec.transactionAmounts === undefined + ? null + : jsonToDb(rec.transactionAmounts), id: rec.depositGroupId, cur: rec.currency, amt: rec.amount, @@ -3698,6 +3754,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { private rowToRefreshGroup(row: ResultRow): WalletRefreshGroup { return { + ...(row.transaction_amounts == null + ? {} + : { transactionAmounts: dbToJson(row.transaction_amounts) }), refreshGroupId: str(row.refresh_group_id), operationStatus: num(row.operation_status), currency: str(row.currency), @@ -3736,16 +3795,17 @@ export class SqliteWalletTransaction implements WalletDbTransaction { async upsertRefreshGroup(rec: WalletRefreshGroup): Promise<void> { await this.run( - `INSERT INTO refresh_groups ( + `INSERT INTO refresh_groups (transaction_amounts, refresh_group_id, operation_status, currency, reason, originating_transaction_id, old_coin_pubs, input_per_coin, expected_output_per_coin, info_per_exchange, status_per_coin, refund_requests, timestamp_created, fail_reason, timestamp_finished - ) VALUES ( + ) VALUES ($transactionAmounts, $id, $status, $cur, $reason, $otid, $ocp, $ipc, $eopc, $ipe, $spc, $rr, $created, $fail, $finished ) ON CONFLICT(refresh_group_id) DO UPDATE SET + transaction_amounts = excluded.transaction_amounts, operation_status = excluded.operation_status, currency = excluded.currency, reason = excluded.reason, @@ -3760,6 +3820,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction { fail_reason = excluded.fail_reason, timestamp_finished = excluded.timestamp_finished`, { + transactionAmounts: + rec.transactionAmounts === undefined + ? null + : jsonToDb(rec.transactionAmounts), id: rec.refreshGroupId, status: rec.operationStatus, cur: rec.currency, @@ -3907,6 +3971,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { download.fulfillmentUrl = str(row.download_fulfillment_url); } return { + ...(row.transaction_amounts == null + ? {} + : { transactionAmounts: dbToJson(row.transaction_amounts) }), proposalId: str(row.proposal_id), orderId: str(row.order_id), merchantBaseUrl: str(row.merchant_base_url), @@ -4062,7 +4129,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction { downloadJson = jsonToDb(rest); } await this.run( - `INSERT INTO purchases ( + `INSERT INTO purchases (transaction_amounts, proposal_id, order_id, merchant_base_url, claim_token, download_session_id, repurchase_proposal_id, purchase_status, abort_refresh_group_id, abort_reason, fail_reason, nonce_priv, @@ -4074,13 +4141,14 @@ export class SqliteWalletTransaction implements WalletDbTransaction { shared, created_from_shared, timestamp, timestamp_accept, timestamp_last_refund_status, timestamp_expired, last_session_id, auto_refund_deadline, refund_amount_awaiting, taler_uri - ) VALUES ( + ) VALUES ($transactionAmounts, $id, $oid, $url, $ct, $dsid, $rpid, $status, $argi, $abort, $fail, $npriv, $npub, $ci, $seed, $dl, $ffu, $pi, $prcp, $tfsp, $mps, $posc, $doi, $dbu, $damt, $dtih, $dtis, $dti, $dy, $shared, $cfs, $ts, $tsa, $tslrs, $tse, $lsid, $ard, $raa, $turi ) ON CONFLICT(proposal_id) DO UPDATE SET + transaction_amounts = excluded.transaction_amounts, order_id = excluded.order_id, merchant_base_url = excluded.merchant_base_url, claim_token = excluded.claim_token, @@ -4121,6 +4189,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction { refund_amount_awaiting = excluded.refund_amount_awaiting, taler_uri = excluded.taler_uri`, { + transactionAmounts: + rec.transactionAmounts === undefined + ? null + : jsonToDb(rec.transactionAmounts), id: rec.proposalId, oid: rec.orderId, url: rec.merchantBaseUrl, diff --git a/packages/taler-wallet-core/src/db/testing/conformance-cases.ts b/packages/taler-wallet-core/src/db/testing/conformance-cases.ts @@ -382,6 +382,12 @@ function makeBankInfo(talerWithdrawUri: string): ReserveBankInfo { function makeWithdrawalGroup(withdrawalGroupId: string): WalletWithdrawalGroup { const wg: WalletWithdrawalGroup = { + transactionAmounts: { + initial: amt("TESTKUDOS:5.2"), + final: amt("TESTKUDOS:0.3"), + inputs: { coin: amt("TESTKUDOS:10") }, + refreshOutputs: { recovery: amt("TESTKUDOS:9.7") }, + }, withdrawalGroupId, wgInfo: { withdrawalType: WithdrawalRecordType.BankManual }, secretSeed: ck(`seed-${withdrawalGroupId}`), @@ -550,6 +556,12 @@ function makeSlate( function makeDepositGroup(depositGroupId: string): WalletDepositGroup { const dg: WalletDepositGroup = { + transactionAmounts: { + initial: amt("TESTKUDOS:5.2"), + final: amt("TESTKUDOS:0.3"), + inputs: { coin: amt("TESTKUDOS:10") }, + refreshOutputs: { recovery: amt("TESTKUDOS:9.7") }, + }, depositGroupId, currency: "TESTKUDOS", amount: amt("TESTKUDOS:5"), @@ -572,6 +584,12 @@ function makeDepositGroup(depositGroupId: string): WalletDepositGroup { function makeRefreshGroup(refreshGroupId: string): WalletRefreshGroup { const rg: WalletRefreshGroup = { + transactionAmounts: { + initial: amt("TESTKUDOS:5.2"), + final: amt("TESTKUDOS:0.3"), + inputs: { coin: amt("TESTKUDOS:10") }, + refreshOutputs: { recovery: amt("TESTKUDOS:9.7") }, + }, refreshGroupId, operationStatus: RefreshOperationStatus.Pending, currency: "TESTKUDOS", @@ -619,6 +637,12 @@ function makeRecoupGroup( function makePeerPushDebit(pursePub: string): WalletPeerPushDebit { const rec: WalletPeerPushDebit = { + transactionAmounts: { + initial: amt("TESTKUDOS:5.2"), + final: amt("TESTKUDOS:0.3"), + inputs: { coin: amt("TESTKUDOS:10") }, + refreshOutputs: { recovery: amt("TESTKUDOS:9.7") }, + }, pursePub: ck(pursePub), exchangeBaseUrl: "https://exchange.test/", amount: amt("TESTKUDOS:3"), @@ -639,6 +663,12 @@ function makePeerPushDebit(pursePub: string): WalletPeerPushDebit { function makePeerPushCredit(peerPushCreditId: string): WalletPeerPushCredit { const rec: WalletPeerPushCredit = { + transactionAmounts: { + initial: amt("TESTKUDOS:5.2"), + final: amt("TESTKUDOS:0.3"), + inputs: { coin: amt("TESTKUDOS:10") }, + refreshOutputs: { recovery: amt("TESTKUDOS:9.7") }, + }, peerPushCreditId, exchangeBaseUrl: "https://exchange.test/", pursePub: ck(`purse-${peerPushCreditId}`), @@ -656,6 +686,12 @@ function makePeerPushCredit(peerPushCreditId: string): WalletPeerPushCredit { function makePeerPullDebit(peerPullDebitId: string): WalletPeerPullDebit { const rec: WalletPeerPullDebit = { + transactionAmounts: { + initial: amt("TESTKUDOS:5.2"), + final: amt("TESTKUDOS:0.3"), + inputs: { coin: amt("TESTKUDOS:10") }, + refreshOutputs: { recovery: amt("TESTKUDOS:9.7") }, + }, peerPullDebitId, pursePub: ck(`purse-${peerPullDebitId}`), exchangeBaseUrl: "https://exchange.test/", @@ -671,6 +707,12 @@ function makePeerPullDebit(peerPullDebitId: string): WalletPeerPullDebit { function makePeerPullCredit(pursePub: string): WalletPeerPullCredit { const rec: WalletPeerPullCredit = { + transactionAmounts: { + initial: amt("TESTKUDOS:5.2"), + final: amt("TESTKUDOS:0.3"), + inputs: { coin: amt("TESTKUDOS:10") }, + refreshOutputs: { recovery: amt("TESTKUDOS:9.7") }, + }, pursePub: ck(pursePub), exchangeBaseUrl: "https://exchange.test/", amount: amt("TESTKUDOS:6"), @@ -709,6 +751,12 @@ function makeRefundGroup( proposalId = "prop-1", ): WalletRefundGroup { const grp: WalletRefundGroup = { + transactionAmounts: { + initial: amt("TESTKUDOS:5.2"), + final: amt("TESTKUDOS:0.3"), + inputs: { coin: amt("TESTKUDOS:10") }, + refreshOutputs: { recovery: amt("TESTKUDOS:9.7") }, + }, refundGroupId, proposalId, status: RefundGroupStatus.Done, @@ -3528,6 +3576,12 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const p = makePurchase("prop-rt"); p.exchanges = ["https://ex-a/", "https://ex-b/"]; + p.transactionAmounts = { + initial: amt("TESTKUDOS:5.2"), + final: amt("TESTKUDOS:0.3"), + inputs: { coin: amt("TESTKUDOS:10") }, + refreshOutputs: { recovery: amt("TESTKUDOS:9.7") }, + }; await runner.runReadWriteTx((tx) => tx.upsertPurchase(p)); const got = await runner.runReadWriteTx((tx) => tx.getPurchase("prop-rt"),