diff options
20 files changed, 237 insertions, 210 deletions
diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/backup-types.ts index 6c7b203b5..5d53f178e 100644 --- a/packages/taler-util/src/backup-types.ts +++ b/packages/taler-util/src/backup-types.ts @@ -475,6 +475,7 @@ export interface BackupRecoupGroup { timestamp_finish?: TalerProtocolTimestamp; finish_clock?: TalerProtocolTimestamp; + // FIXME: Use some enum here! finish_is_failure?: boolean; /** @@ -483,7 +484,6 @@ export interface BackupRecoupGroup { coins: { coin_pub: string; recoup_finished: boolean; - old_amount: BackupAmountString; }[]; } @@ -582,9 +582,14 @@ export interface BackupCoin { denom_sig: UnblindedSignature; /** - * Amount that's left on the coin. + * Information about where and how the coin was spent. */ - current_amount: BackupAmountString; + spend_allocation: + | { + id: string; + amount: BackupAmountString; + } + | undefined; /** * Blinding key used when withdrawing the coin. diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index de88fef69..71ceb7939 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -968,60 +968,6 @@ export class WithdrawBatchResponse { ev_sigs: WithdrawResponse[]; } -/** - * Easy to process format for the public data of coins - * managed by the wallet. - */ -export interface CoinDumpJson { - coins: Array<{ - /** - * The coin's denomination's public key. - */ - denom_pub: DenominationPubKey; - /** - * Hash of denom_pub. - */ - denom_pub_hash: string; - /** - * Value of the denomination (without any fees). - */ - denom_value: string; - /** - * Public key of the coin. - */ - coin_pub: string; - /** - * Base URL of the exchange for the coin. - */ - exchange_base_url: string; - /** - * Remaining value on the coin, to the knowledge of - * the wallet. - */ - remaining_value: string; - /** - * Public key of the parent coin. - * Only present if this coin was obtained via refreshing. - */ - refresh_parent_coin_pub: string | undefined; - /** - * Public key of the reserve for this coin. - * Only present if this coin was obtained via refreshing. - */ - withdrawal_reserve_pub: string | undefined; - /** - * Is the coin suspended? - * Suspended coins are not considered for payments. - */ - coin_suspended: boolean; - - /** - * Information about the age restriction - */ - ageCommitmentProof: AgeCommitmentProof | undefined; - }>; -} - export interface MerchantPayResponse { sig: string; } diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index d4de53972..54f4c54a2 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -63,7 +63,10 @@ import { ExchangeAuditor, UnblindedSignature, } from "./taler-types.js"; -import { OrderShortInfo, codecForOrderShortInfo } from "./transactions-types.js"; +import { + OrderShortInfo, + codecForOrderShortInfo, +} from "./transactions-types.js"; import { BackupRecovery } from "./backup-types.js"; import { PaytoUri } from "./payto.js"; import { TalerErrorCode } from "./taler-error-codes.js"; @@ -141,6 +144,77 @@ export function mkAmount( return { value, fraction, currency }; } +/** + * Status of a coin. + */ +export enum CoinStatus { + /** + * Withdrawn and never shown to anybody. + */ + Fresh = "fresh", + + /** + * Fresh, but currently marked as "suspended", thus won't be used + * for spending. Used for testing. + */ + FreshSuspended = "fresh-suspended", + + /** + * A coin that has been spent and refreshed. + */ + Dormant = "dormant", +} + +/** + * Easy to process format for the public data of coins + * managed by the wallet. + */ +export interface CoinDumpJson { + coins: Array<{ + /** + * The coin's denomination's public key. + */ + denom_pub: DenominationPubKey; + /** + * Hash of denom_pub. + */ + denom_pub_hash: string; + /** + * Value of the denomination (without any fees). + */ + denom_value: string; + /** + * Public key of the coin. + */ + coin_pub: string; + /** + * Base URL of the exchange for the coin. + */ + exchange_base_url: string; + /** + * Public key of the parent coin. + * Only present if this coin was obtained via refreshing. + */ + refresh_parent_coin_pub: string | undefined; + /** + * Public key of the reserve for this coin. + * Only present if this coin was obtained via refreshing. + */ + withdrawal_reserve_pub: string | undefined; + coin_status: CoinStatus; + spend_allocation: + | { + id: string; + amount: string; + } + | undefined; + /** + * Information about the age restriction + */ + ageCommitmentProof: AgeCommitmentProof | undefined; + }>; +} + export enum ConfirmPayResultType { Done = "done", Pending = "pending", @@ -568,10 +642,11 @@ export enum RefreshReason { } /** - * Wrapper for coin public keys. + * Request to refresh a single coin. */ -export interface CoinPublicKey { +export interface CoinRefreshRequest { readonly coinPub: string; + readonly amount: AmountJson; } /** diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 941a2f28f..b3abbac6f 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -1105,9 +1105,7 @@ advancedCli console.log(`coin ${coin.coin_pub}`); console.log(` exchange ${coin.exchange_base_url}`); console.log(` denomPubHash ${coin.denom_pub_hash}`); - console.log( - ` remaining amount ${Amounts.stringify(coin.remaining_value)}`, - ); + console.log(` status ${coin.coin_status}`); } }); }); diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts index e4e96a180..8d1f6e873 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts @@ -193,7 +193,7 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { .map((x) => x.amountEffective), ).amount; - t.assertAmountEquals("TESTKUDOS:8.33", effective); + t.assertAmountEquals("TESTKUDOS:8.59", effective); } await t.shutdown(); diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts index c16a85f19..03c446db3 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts @@ -22,7 +22,7 @@ /** * Imports. */ -import { Amounts } from "@gnu-taler/taler-util"; +import { Amounts, CoinStatus } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { @@ -32,7 +32,7 @@ import { MerchantService, setupDb, WalletCli, - getPayto + getPayto, } from "../harness/harness.js"; import { SimpleTestEnvironment } from "../harness/helpers.js"; @@ -184,7 +184,10 @@ export async function runWallettestingTest(t: GlobalTestState) { let susp: string | undefined; { for (const c of coinDump.coins) { - if (0 === Amounts.cmp(c.remaining_value, "TESTKUDOS:8")) { + if ( + c.coin_status === CoinStatus.Fresh && + 0 === Amounts.cmp(c.denom_value, "TESTKUDOS:8") + ) { susp = c.coin_pub; } } diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index c301ee457..b785efed8 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -49,6 +49,8 @@ import { ExchangeGlobalFees, DenomSelectionState, TransactionIdStr, + CoinRefreshRequest, + CoinStatus, } from "@gnu-taler/taler-util"; import { RetryInfo, RetryTags } from "./util/retries.js"; import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; @@ -603,27 +605,6 @@ export interface PlanchetRecord { ageCommitmentProof?: AgeCommitmentProof; } -/** - * Status of a coin. - */ -export enum CoinStatus { - /** - * Withdrawn and never shown to anybody. - */ - Fresh = "fresh", - - /** - * Fresh, but currently marked as "suspended", thus won't be used - * for spending. Used for testing. - */ - FreshSuspended = "fresh-suspended", - - /** - * A coin that has been spent and refreshed. - */ - Dormant = "dormant", -} - export enum CoinSourceType { Withdraw = "withdraw", Refresh = "refresh", @@ -693,14 +674,6 @@ export interface CoinRecord { denomSig: UnblindedSignature; /** - * Amount that's left on the coin. - * - * FIXME: This is pretty redundant with "allocation" and "status". - * Do we really need this? - */ - currentAmount: AmountJson; - - /** * Base URL that identifies the exchange from which we got the * coin. */ @@ -732,7 +705,7 @@ export interface CoinRecord { * - Diagnostics * - Idempotency of applying a coin selection (e.g. after re-selection) */ - allocation: CoinAllocation | undefined; + spendAllocation: CoinAllocation | undefined; /** * Maximum age of purchases that can be made with this coin. @@ -1462,17 +1435,10 @@ export interface RecoupGroupRecord { recoupFinishedPerCoin: boolean[]; /** - * We store old amount (i.e. before recoup) of recouped coins here, - * as the balance of a recouped coin is set to zero when the - * recoup group is created. - */ - oldAmountPerCoin: AmountJson[]; - - /** * Public keys of coins that should be scheduled for refreshing * after all individual recoups are done. */ - scheduleRefreshCoins: string[]; + scheduleRefreshCoins: CoinRefreshRequest[]; } export enum BackupProviderStateTag { @@ -1875,7 +1841,6 @@ export const WalletStoresV1 = { "exchangeTos", describeContents<ExchangeTosRecord>({ keyPath: ["exchangeBaseUrl", "etag"], - autoIncrement: true, }), {}, ), diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index bc956bd17..ebb9cdb9b 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -38,7 +38,7 @@ import { CancellationToken, DenominationInfo, RefreshGroupId, - CoinPublicKey, + CoinRefreshRequest, RefreshReason, } from "@gnu-taler/taler-util"; import { CryptoDispatcher } from "./crypto/workers/cryptoDispatcher.js"; @@ -86,7 +86,7 @@ export interface RefreshOperations { refreshGroups: typeof WalletStoresV1.refreshGroups; coinAvailability: typeof WalletStoresV1.coinAvailability; }>, - oldCoinPubs: CoinPublicKey[], + oldCoinPubs: CoinRefreshRequest[], reason: RefreshReason, ): Promise<RefreshGroupId>; } diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index 30e61e382..1472b1b90 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -54,6 +54,7 @@ import { BACKUP_VERSION_MINOR, canonicalizeBaseUrl, canonicalJson, + CoinStatus, encodeCrock, getRandomBytes, hash, @@ -63,7 +64,6 @@ import { } from "@gnu-taler/taler-util"; import { CoinSourceType, - CoinStatus, ConfigRecordKey, DenominationRecord, PurchaseStatus, @@ -206,7 +206,6 @@ export async function exportBackup( coins: recoupGroup.coinPubs.map((x, i) => ({ coin_pub: x, recoup_finished: recoupGroup.recoupFinishedPerCoin[i], - old_amount: Amounts.stringify(recoupGroup.oldAmountPerCoin[i]), })), }); }); @@ -259,8 +258,13 @@ export async function exportBackup( blinding_key: coin.blindingKey, coin_priv: coin.coinPriv, coin_source: bcs, - current_amount: Amounts.stringify(coin.currentAmount), fresh: coin.status === CoinStatus.Fresh, + spend_allocation: coin.spendAllocation + ? { + amount: coin.spendAllocation.amount, + id: coin.spendAllocation.id, + } + : undefined, denom_sig: coin.denomSig, }); }); diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 3bbb7d798..9c5eea9af 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -27,6 +27,7 @@ import { BackupRefundState, BackupWgType, codecForContractTerms, + CoinStatus, DenomKeyType, DenomSelectionState, j2s, @@ -41,10 +42,8 @@ import { CoinRecord, CoinSource, CoinSourceType, - CoinStatus, DenominationRecord, DenominationVerificationStatus, - OperationStatus, ProposalDownloadInfo, PurchaseStatus, PurchasePayInfo, @@ -272,7 +271,6 @@ export async function importCoin( blindingKey: backupCoin.blinding_key, coinEvHash: compCoin.coinEvHash, coinPriv: backupCoin.coin_priv, - currentAmount: Amounts.parseOrThrow(backupCoin.current_amount), denomSig: backupCoin.denom_sig, coinPub: compCoin.coinPub, exchangeBaseUrl, @@ -284,7 +282,7 @@ export async function importCoin( // FIXME! ageCommitmentProof: undefined, // FIXME! - allocation: undefined, + spendAllocation: undefined, }; if (coinRecord.status === CoinStatus.Fresh) { await makeCoinAvailable(ws, tx, coinRecord); diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts index 44357fdf4..3db66b5d9 100644 --- a/packages/taler-wallet-core/src/operations/balance.ts +++ b/packages/taler-wallet-core/src/operations/balance.ts @@ -23,7 +23,7 @@ import { Amounts, Logger, } from "@gnu-taler/taler-util"; -import { CoinStatus, WalletStoresV1 } from "../db.js"; +import { WalletStoresV1 } from "../db.js"; import { GetReadOnlyAccess } from "../util/query.js"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -42,6 +42,7 @@ export async function getBalancesInsideTransaction( ws: InternalWalletState, tx: GetReadOnlyAccess<{ coins: typeof WalletStoresV1.coins; + coinAvailability: typeof WalletStoresV1.coinAvailability; refreshGroups: typeof WalletStoresV1.refreshGroups; withdrawalGroups: typeof WalletStoresV1.withdrawalGroups; }>, @@ -64,12 +65,14 @@ export async function getBalancesInsideTransaction( return balanceStore[currency]; }; - await tx.coins.iter().forEach((c) => { - // Only count fresh coins, as dormant coins will - // already be in a refresh session. - if (c.status === CoinStatus.Fresh) { - const b = initBalance(c.currentAmount.currency); - b.available = Amounts.add(b.available, c.currentAmount).amount; + await tx.coinAvailability.iter().forEach((ca) => { + const b = initBalance(ca.currency); + for (let i = 0; i < ca.freshCoinCount; i++) { + b.available = Amounts.add(b.available, { + currency: ca.currency, + fraction: ca.amountFrac, + value: ca.amountVal, + }).amount; } }); @@ -139,7 +142,13 @@ export async function getBalances( logger.trace("starting to compute balance"); const wbal = await ws.db - .mktx((x) => [x.coins, x.refreshGroups, x.purchases, x.withdrawalGroups]) + .mktx((x) => [ + x.coins, + x.coinAvailability, + x.refreshGroups, + x.purchases, + x.withdrawalGroups, + ]) .runReadOnly(async (tx) => { return getBalancesInsideTransaction(ws, tx); }); diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index d17530c7f..5e02f3d7b 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -20,6 +20,8 @@ import { AmountJson, Amounts, + CoinRefreshRequest, + CoinStatus, j2s, Logger, RefreshReason, @@ -29,7 +31,7 @@ import { TransactionIdStr, TransactionType, } from "@gnu-taler/taler-util"; -import { WalletStoresV1, CoinStatus, CoinRecord } from "../db.js"; +import { WalletStoresV1, CoinRecord } from "../db.js"; import { makeErrorDetail, TalerError } from "../errors.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; @@ -103,11 +105,19 @@ export async function spendCoins( }>, csi: CoinsSpendInfo, ): Promise<void> { + let refreshCoinPubs: CoinRefreshRequest[] = []; for (let i = 0; i < csi.coinPubs.length; i++) { const coin = await tx.coins.get(csi.coinPubs[i]); if (!coin) { throw Error("coin allocated for payment doesn't exist anymore"); } + const denom = await ws.getDenomInfo( + ws, + tx, + coin.exchangeBaseUrl, + coin.denomPubHash, + ); + checkDbInvariant(!!denom); const coinAvailability = await tx.coinAvailability.get([ coin.exchangeBaseUrl, coin.denomPubHash, @@ -116,7 +126,7 @@ export async function spendCoins( checkDbInvariant(!!coinAvailability); const contrib = csi.contributions[i]; if (coin.status !== CoinStatus.Fresh) { - const alloc = coin.allocation; + const alloc = coin.spendAllocation; if (!alloc) { continue; } @@ -131,15 +141,18 @@ export async function spendCoins( continue; } coin.status = CoinStatus.Dormant; - coin.allocation = { + coin.spendAllocation = { id: csi.allocationId, amount: Amounts.stringify(contrib), }; - const remaining = Amounts.sub(coin.currentAmount, contrib); + const remaining = Amounts.sub(denom.value, contrib); if (remaining.saturated) { throw Error("not enough remaining balance on coin for payment"); } - coin.currentAmount = remaining.amount; + refreshCoinPubs.push({ + amount: remaining.amount, + coinPub: coin.coinPub, + }); checkDbInvariant(!!coinAvailability); if (coinAvailability.freshCoinCount === 0) { throw Error( @@ -150,9 +163,6 @@ export async function spendCoins( await tx.coins.put(coin); await tx.coinAvailability.put(coinAvailability); } - const refreshCoinPubs = csi.coinPubs.map((x) => ({ - coinPub: x, - })); await ws.refreshOps.createRefreshGroup( ws, tx, diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index 6b14b60c6..2b0ea1f96 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -40,7 +40,8 @@ import { codecForMerchantPayResponse, codecForProposal, CoinDepositPermission, - CoinPublicKey, + CoinRefreshRequest, + CoinStatus, ConfirmPayResult, ConfirmPayResultType, ContractTerms, @@ -78,7 +79,6 @@ import { AllowedExchangeInfo, BackupProviderStateTag, CoinRecord, - CoinStatus, DenominationRecord, PurchaseRecord, PurchaseStatus, @@ -2084,7 +2084,7 @@ async function applySuccessfulRefund( denominations: typeof WalletStoresV1.denominations; }>, p: PurchaseRecord, - refreshCoinsMap: Record<string, { coinPub: string }>, + refreshCoinsMap: Record<string, CoinRefreshRequest>, r: MerchantCoinRefundSuccessStatus, ): Promise<void> { // FIXME: check signature before storing it as valid! @@ -2102,31 +2102,23 @@ async function applySuccessfulRefund( if (!denom) { throw Error("inconsistent database"); } - refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub }; const refundAmount = Amounts.parseOrThrow(r.refund_amount); const refundFee = denom.fees.feeRefund; + const amountLeft = Amounts.sub(refundAmount, refundFee).amount; coin.status = CoinStatus.Dormant; - coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount; - coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount; - logger.trace(`coin amount after is ${Amounts.stringify(coin.currentAmount)}`); await tx.coins.put(coin); const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl .iter(coin.exchangeBaseUrl) .toArray(); - - const amountLeft = Amounts.sub( - Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount)) - .amount, - denom.fees.feeRefund, - ).amount; - const totalRefreshCostBound = getTotalRefreshCost( allDenoms, DenominationRecord.toDenomInfo(denom), amountLeft, ); + refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub, amount: amountLeft }; + p.refunds[refundKey] = { type: RefundState.Applied, obtainedTime: AbsoluteTime.toTimestamp(AbsoluteTime.now()), @@ -2167,9 +2159,9 @@ async function storePendingRefund( .iter(coin.exchangeBaseUrl) .toArray(); + // Refunded amount after fees. const amountLeft = Amounts.sub( - Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount)) - .amount, + Amounts.parseOrThrow(r.refund_amount), denom.fees.feeRefund, ).amount; @@ -2197,7 +2189,7 @@ async function storeFailedRefund( denominations: typeof WalletStoresV1.denominations; }>, p: PurchaseRecord, - refreshCoinsMap: Record<string, { coinPub: string }>, + refreshCoinsMap: Record<string, CoinRefreshRequest>, r: MerchantCoinRefundFailureStatus, ): Promise<void> { const refundKey = getRefundKey(r); @@ -2221,8 +2213,7 @@ async function storeFailedRefund( .toArray(); const amountLeft = Amounts.sub( - Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount)) - .amount, + Amounts.parseOrThrow(r.refund_amount), denom.fees.feeRefund, ).amount; @@ -2246,6 +2237,7 @@ async function storeFailedRefund( if (p.purchaseStatus === PurchaseStatus.AbortingWithRefund) { // Refund failed because the merchant didn't even try to deposit // the coin yet, so we try to refresh. + // FIXME: Is this case tested?! if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) { const coin = await tx.coins.get(r.coin_pub); if (!coin) { @@ -2271,14 +2263,11 @@ async function storeFailedRefund( contrib = payCoinSelection.coinContributions[i]; } } - if (contrib) { - coin.currentAmount = Amounts.add(coin.currentAmount, contrib).amount; - coin.currentAmount = Amounts.sub( - coin.currentAmount, - denom.fees.feeRefund, - ).amount; - } - refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub }; + // FIXME: Is this case tested?! + refreshCoinsMap[coin.coinPub] = { + coinPub: coin.coinPub, + amount: amountLeft, + }; await tx.coins.put(coin); } } @@ -2308,7 +2297,7 @@ async function acceptRefunds( return; } - const refreshCoinsMap: Record<string, CoinPublicKey> = {}; + const refreshCoinsMap: Record<string, CoinRefreshRequest> = {}; for (const refundStatus of refunds) { const refundKey = getRefundKey(refundStatus); @@ -2350,6 +2339,7 @@ async function acceptRefunds( } const refreshCoinsPubs = Object.values(refreshCoinsMap); + logger.info(`refreshCoinMap ${j2s(refreshCoinsMap)}`); if (refreshCoinsPubs.length > 0) { await createRefreshGroup( ws, diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts index ffc49c24c..3b65fba6b 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer.ts @@ -36,6 +36,7 @@ import { codecForAmountString, codecForAny, codecForExchangeGetContractResponse, + CoinStatus, constructPayPullUri, constructPayPushUri, ContractTermsUtil, @@ -63,17 +64,16 @@ import { WalletAccountMergeFlags, } from "@gnu-taler/taler-util"; import { - CoinStatus, - WithdrawalGroupStatus, + ReserveRecord, WalletStoresV1, + WithdrawalGroupStatus, WithdrawalRecordType, - ReserveRecord, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; +import { makeTransactionId, spendCoins } from "../operations/common.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { checkDbInvariant } from "../util/invariants.js"; import { GetReadOnlyAccess } from "../util/query.js"; -import { spendCoins, makeTransactionId } from "../operations/common.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { internalCreateWithdrawalGroup } from "./withdraw.js"; diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index ff6bb4efc..d3bcde048 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -28,6 +28,7 @@ import { Amounts, codecForRecoupConfirmation, codecForReserveStatus, + CoinStatus, encodeCrock, getRandomBytes, j2s, @@ -40,7 +41,6 @@ import { import { CoinRecord, CoinSourceType, - CoinStatus, RecoupGroupRecord, RefreshCoinSource, WalletStoresV1, @@ -50,6 +50,7 @@ import { } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js"; +import { checkDbInvariant } from "../util/invariants.js"; import { GetReadWriteAccess } from "../util/query.js"; import { OperationAttemptResult, @@ -180,8 +181,6 @@ async function recoupWithdrawCoin( return; } updatedCoin.status = CoinStatus.Dormant; - const currency = updatedCoin.currentAmount.currency; - updatedCoin.currentAmount = Amounts.getZero(currency); await tx.coins.put(updatedCoin); await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); }); @@ -265,16 +264,25 @@ async function recoupRefreshCoin( logger.warn("refresh old coin for recoup not found"); return; } - revokedCoin.status = CoinStatus.Dormant; - oldCoin.currentAmount = Amounts.add( - oldCoin.currentAmount, - recoupGroup.oldAmountPerCoin[coinIdx], - ).amount; - logger.trace( - "recoup: setting old coin amount to", - Amounts.stringify(oldCoin.currentAmount), + const oldCoinDenom = await ws.getDenomInfo( + ws, + tx, + oldCoin.exchangeBaseUrl, + oldCoin.denomPubHash, ); - recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub); + const revokedCoinDenom = await ws.getDenomInfo( + ws, + tx, + revokedCoin.exchangeBaseUrl, + revokedCoin.denomPubHash, + ); + checkDbInvariant(!!oldCoinDenom); + checkDbInvariant(!!revokedCoinDenom); + revokedCoin.status = CoinStatus.Dormant; + recoupGroup.scheduleRefreshCoins.push({ + coinPub: oldCoin.coinPub, + amount: Amounts.sub(oldCoinDenom.value, revokedCoinDenom.value).amount, + }); await tx.coins.put(revokedCoin); await tx.coins.put(oldCoin); await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); @@ -410,7 +418,7 @@ export async function processRecoupGroupHandler( const refreshGroupId = await createRefreshGroup( ws, tx, - rg2.scheduleRefreshCoins.map((x) => ({ coinPub: x })), + rg2.scheduleRefreshCoins, RefreshReason.Recoup, ); processRefreshGroup(ws, refreshGroupId.refreshGroupId).catch((e) => { @@ -442,8 +450,6 @@ export async function createRecoupGroup( timestampFinished: undefined, timestampStarted: TalerProtocolTimestamp.now(), recoupFinishedPerCoin: coinPubs.map(() => false), - // Will be populated later - oldAmountPerCoin: [], scheduleRefreshCoins: [], }; @@ -454,12 +460,6 @@ export async function createRecoupGroup( await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); continue; } - if (Amounts.isZero(coin.currentAmount)) { - await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); - continue; - } - recoupGroup.oldAmountPerCoin[coinIdx] = coin.currentAmount; - coin.currentAmount = Amounts.getZero(coin.currentAmount.currency); await tx.coins.put(coin); } diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 83ab32f20..c7d2c320e 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -23,8 +23,9 @@ import { amountToPretty, codecForExchangeMeltResponse, codecForExchangeRevealResponse, - CoinPublicKey, CoinPublicKeyString, + CoinRefreshRequest, + CoinStatus, DenominationInfo, DenomKeyType, Duration, @@ -55,9 +56,7 @@ import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js"; import { CoinRecord, CoinSourceType, - CoinStatus, DenominationRecord, - OperationStatus, RefreshCoinStatus, RefreshGroupRecord, RefreshOperationStatus, @@ -672,7 +671,6 @@ async function refreshReveal( blindingKey: pc.blindingKey, coinPriv: pc.coinPriv, coinPub: pc.coinPub, - currentAmount: ncd.value, denomPubHash: ncd.denomPubHash, denomSig, exchangeBaseUrl: oldCoin.exchangeBaseUrl, @@ -684,7 +682,7 @@ async function refreshReveal( coinEvHash: pc.coinEvHash, maxAge: pc.maxAge, ageCommitmentProof: pc.ageCommitmentProof, - allocation: undefined, + spendAllocation: undefined, }; coins.push(coin); @@ -845,7 +843,7 @@ export async function createRefreshGroup( refreshGroups: typeof WalletStoresV1.refreshGroups; coinAvailability: typeof WalletStoresV1.coinAvailability; }>, - oldCoinPubs: CoinPublicKey[], + oldCoinPubs: CoinRefreshRequest[], reason: RefreshReason, ): Promise<RefreshGroupId> { const refreshGroupId = encodeCrock(getRandomBytes(32)); @@ -908,9 +906,8 @@ export async function createRefreshGroup( default: assertUnreachable(coin.status); } - const refreshAmount = coin.currentAmount; + const refreshAmount = ocp.amount; inputPerCoin.push(refreshAmount); - coin.currentAmount = Amounts.getZero(refreshAmount.currency); await tx.coins.put(coin); const denoms = await getDenoms(coin.exchangeBaseUrl); const cost = getTotalRefreshCost(denoms, denom, refreshAmount); @@ -1008,7 +1005,7 @@ export async function autoRefresh( const coins = await tx.coins.indexes.byBaseUrl .iter(exchangeBaseUrl) .toArray(); - const refreshCoins: CoinPublicKey[] = []; + const refreshCoins: CoinRefreshRequest[] = []; for (const coin of coins) { if (coin.status !== CoinStatus.Fresh) { continue; @@ -1023,7 +1020,14 @@ export async function autoRefresh( } const executeThreshold = getAutoRefreshExecuteThreshold(denom); if (AbsoluteTime.isExpired(executeThreshold)) { - refreshCoins.push(coin); + refreshCoins.push({ + coinPub: coin.coinPub, + amount: { + value: denom.amountVal, + fraction: denom.amountFrac, + currency: denom.currency, + }, + }); } else { const checkThreshold = getAutoRefreshCheckThreshold(denom); minCheckThreshold = AbsoluteTime.min( diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index b74e1182a..f98d69e26 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -24,6 +24,7 @@ import { BlindedDenominationSignature, codecForMerchantTipResponseV2, codecForTipPickupGetResponse, + CoinStatus, DenomKeyType, encodeCrock, getRandomBytes, @@ -41,7 +42,6 @@ import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js"; import { CoinRecord, CoinSourceType, - CoinStatus, DenominationRecord, TipRecord, } from "../db.js"; @@ -311,7 +311,6 @@ export async function processTip( coinIndex: i, walletTipId: walletTipId, }, - currentAmount: DenominationRecord.getValue(denom), denomPubHash: denom.denomPubHash, denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig }, exchangeBaseUrl: tipRecord.exchangeBaseUrl, @@ -319,7 +318,7 @@ export async function processTip( coinEvHash: planchet.coinEvHash, maxAge: AgeRestriction.AGE_UNRESTRICTED, ageCommitmentProof: planchet.ageCommitmentProof, - allocation: undefined, + spendAllocation: undefined, }); } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index c7ff4161a..1e7f982bc 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -540,7 +540,6 @@ function buildTransactionForTip( /** * For a set of refund with the same executionTime. - * */ interface MergedRefundInfo { executionTime: TalerProtocolTimestamp; @@ -556,7 +555,7 @@ function mergeRefundByExecutionTime( const refundByExecTime = rs.reduce((prev, refund) => { const key = `${refund.executionTime.t_s}`; - //refunds counts if applied + // refunds count if applied const effective = refund.type === RefundState.Applied ? Amounts.sub( @@ -582,7 +581,10 @@ function mergeRefundByExecutionTime( v.amountAppliedEffective, effective, ).amount; - v.amountAppliedRaw = Amounts.add(v.amountAppliedRaw).amount; + v.amountAppliedRaw = Amounts.add( + v.amountAppliedRaw, + refund.refundAmount, + ).amount; v.firstTimestamp = TalerProtocolTimestamp.min( v.firstTimestamp, refund.obtainedTime, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index a258c5d76..d7627e6cf 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -36,6 +36,7 @@ import { codecForWithdrawBatchResponse, codecForWithdrawOperationStatusResponse, codecForWithdrawResponse, + CoinStatus, DenomKeyType, DenomSelectionState, Duration, @@ -57,7 +58,6 @@ import { TransactionType, UnblindedSignature, URL, - VersionMatchResult, WithdrawBatchResponse, WithdrawResponse, WithdrawUriInfoResponse, @@ -66,10 +66,8 @@ import { EddsaKeypair } from "../crypto/cryptoImplementation.js"; import { CoinRecord, CoinSourceType, - CoinStatus, DenominationRecord, DenominationVerificationStatus, - ExchangeTosRecord, PlanchetRecord, PlanchetStatus, WalletStoresV1, @@ -736,7 +734,6 @@ async function processPlanchetVerifyAndStoreCoin( blindingKey: planchet.blindingKey, coinPriv: planchet.coinPriv, coinPub: planchet.coinPub, - currentAmount: denomInfo.value, denomPubHash: planchet.denomPubHash, denomSig, coinEvHash: planchet.coinEvHash, @@ -750,7 +747,7 @@ async function processPlanchetVerifyAndStoreCoin( }, maxAge: planchet.maxAge, ageCommitmentProof: planchet.ageCommitmentProof, - allocation: undefined, + spendAllocation: undefined, }; const planchetCoinPub = planchet.coinPub; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index ef7a745ab..ef41c5101 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -95,6 +95,8 @@ import { WalletNotification, codecForSetDevModeRequest, ExchangeTosStatusDetails, + CoinRefreshRequest, + CoinStatus, } from "@gnu-taler/taler-util"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { @@ -105,11 +107,9 @@ import { clearDatabase } from "./db-utils.js"; import { AuditorTrustRecord, CoinSourceType, - CoinStatus, ConfigRecordKey, DenominationRecord, ExchangeDetailsRecord, - ExchangeTosRecord, exportDb, importDb, WalletStoresV1, @@ -934,10 +934,15 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> { }), exchange_base_url: c.exchangeBaseUrl, refresh_parent_coin_pub: refreshParentCoinPub, - remaining_value: Amounts.stringify(c.currentAmount), withdrawal_reserve_pub: withdrawalReservePub, - coin_suspended: c.status === CoinStatus.FreshSuspended, + coin_status: c.status, ageCommitmentProof: c.ageCommitmentProof, + spend_allocation: c.spendAllocation + ? { + amount: c.spendAllocation.amount, + id: c.spendAllocation.id, + } + : undefined, }); } }); @@ -1153,7 +1158,6 @@ async function dispatchRequestInternal( } case "forceRefresh": { const req = codecForForceRefreshRequest().decode(payload); - const coinPubs = req.coinPubList.map((x) => ({ coinPub: x })); const refreshGroupId = await ws.db .mktx((x) => [ x.refreshGroups, @@ -1162,6 +1166,24 @@ async function dispatchRequestInternal( x.coins, ]) .runReadWrite(async (tx) => { + let coinPubs: CoinRefreshRequest[] = []; + for (const c of req.coinPubList) { + const coin = await tx.coins.get(c); + if (!coin) { + throw Error(`coin (pubkey ${c}) not found`); + } + const denom = await ws.getDenomInfo( + ws, + tx, + coin.exchangeBaseUrl, + coin.denomPubHash, + ); + checkDbInvariant(!!denom); + coinPubs.push({ + coinPub: c, + amount: denom?.value, + }); + } return await createRefreshGroup( ws, tx, |