diff options
Diffstat (limited to 'packages')
13 files changed, 167 insertions, 174 deletions
diff --git a/packages/taler-integrationtests/src/test-tipping.ts b/packages/taler-integrationtests/src/test-tipping.ts index ddf56c0e0..f7840f5da 100644 --- a/packages/taler-integrationtests/src/test-tipping.ts +++ b/packages/taler-integrationtests/src/test-tipping.ts @@ -94,13 +94,19 @@ runTest(async (t: GlobalTestState) => { console.log(ptr); + t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5"); + t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85"); + await wallet.acceptTip({ walletTipId: ptr.walletTipId, }); + await wallet.runUntilDone(); const bal = await wallet.getBalances(); console.log(bal); + + t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:4.85"); }); diff --git a/packages/taler-wallet-core/src/TalerErrorCode.ts b/packages/taler-wallet-core/src/TalerErrorCode.ts index 8a020b9da..e1f777f25 100644 --- a/packages/taler-wallet-core/src/TalerErrorCode.ts +++ b/packages/taler-wallet-core/src/TalerErrorCode.ts @@ -22,6 +22,8 @@ */ export enum TalerErrorCode { + + /** * Special code to indicate no error (or no "code" present). * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). @@ -3271,9 +3273,17 @@ export enum TalerErrorCode { WALLET_WITHDRAWAL_GROUP_INCOMPLETE = 7015, /** + * The signature on a coin by the exchange's denomination key (obtained through the merchant via tipping) is invalid after unblinding it. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_TIPPING_COIN_SIGNATURE_INVALID = 7016, + + /** * End of error code range. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). */ END = 9999, + } diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 91a55c705..7dda1214d 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -286,7 +286,6 @@ async function gatherWithdrawalPending( givesLifeness: true, numCoinsTotal, numCoinsWithdrawn, - source: wsr.source, withdrawalGroupId: wsr.withdrawalGroupId, lastError: wsr.lastError, retryInfo: wsr.retryInfo, diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index 69942fe94..b4fa3b23e 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -818,10 +818,7 @@ async function depleteReserve( const withdrawalRecord: WithdrawalGroupRecord = { withdrawalGroupId: withdrawalGroupId, exchangeBaseUrl: newReserve.exchangeBaseUrl, - source: { - type: WithdrawalSourceType.Reserve, - reservePub: newReserve.reservePub, - }, + reservePub: newReserve.reservePub, rawWithdrawalAmount: withdrawAmount, timestampStart: getTimestampNow(), retryInfo: initRetryInfo(), diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 6fe374bf0..6ccd262b0 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -31,6 +31,9 @@ import { updateRetryInfoTimeout, WithdrawalSourceType, TipPlanchet, + CoinRecord, + CoinSourceType, + CoinStatus, } from "../types/dbTypes"; import { getExchangeWithdrawalInfo, @@ -40,13 +43,14 @@ import { } from "./withdraw"; import { updateExchangeFromUrl } from "./exchanges"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; -import { guardOperationException } from "./errors"; +import { guardOperationException, makeErrorDetails } from "./errors"; import { NotificationType } from "../types/notifications"; import { getTimestampNow } from "../util/time"; import { readSuccessResponseJsonOrThrow } from "../util/http"; import { URL } from "../util/url"; import { Logger } from "../util/logging"; import { checkDbInvariant } from "../util/invariants"; +import { TalerErrorCode } from "../TalerErrorCode"; const logger = new Logger("operations/tip.ts"); @@ -99,7 +103,7 @@ export async function prepareTip( walletTipId: walletTipId, acceptedTimestamp: undefined, rejectedTimestamp: undefined, - amount, + tipAmountRaw: amount, deadline: tipPickupStatus.expiration, exchangeUrl: tipPickupStatus.exchange_url, merchantBaseUrl: res.merchantBaseUrl, @@ -109,10 +113,10 @@ export async function prepareTip( response: undefined, createdTimestamp: getTimestampNow(), merchantTipId: res.merchantTipId, - totalFees: Amounts.add( + tipAmountEffective: Amounts.sub(amount, Amounts.add( withdrawDetails.overhead, withdrawDetails.withdrawFee, - ).amount, + ).amount).amount, retryInfo: initRetryInfo(), lastError: undefined, denomsSel: denomSelectionInfoToState(selectedDenoms), @@ -122,10 +126,10 @@ export async function prepareTip( const tipStatus: PrepareTipResult = { accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, - amount: Amounts.stringify(tipPickupStatus.tip_amount), + tipAmountRaw: Amounts.stringify(tipPickupStatus.tip_amount), exchangeBaseUrl: tipPickupStatus.exchange_url, expirationTimestamp: tipPickupStatus.expiration, - totalFees: Amounts.stringify(tipRecord.totalFees), + tipAmountEffective: Amounts.stringify(tipRecord.tipAmountEffective), walletTipId: tipRecord.walletTipId, }; @@ -182,13 +186,13 @@ async function resetTipRetry( async function processTipImpl( ws: InternalWalletState, - tipId: string, + walletTipId: string, forceNow: boolean, ): Promise<void> { if (forceNow) { - await resetTipRetry(ws, tipId); + await resetTipRetry(ws, walletTipId); } - let tipRecord = await ws.db.get(Stores.tips, tipId); + let tipRecord = await ws.db.get(Stores.tips, walletTipId); if (!tipRecord) { return; } @@ -216,7 +220,7 @@ async function processTipImpl( planchets.push(r); } } - await ws.db.mutate(Stores.tips, tipId, (r) => { + await ws.db.mutate(Stores.tips, walletTipId, (r) => { if (!r.planchets) { r.planchets = planchets; } @@ -224,7 +228,7 @@ async function processTipImpl( }); } - tipRecord = await ws.db.get(Stores.tips, tipId); + tipRecord = await ws.db.get(Stores.tips, walletTipId); checkDbInvariant(!!tipRecord, "tip record should be in database"); checkDbInvariant(!!tipRecord.planchets, "tip record should have planchets"); @@ -246,55 +250,68 @@ async function processTipImpl( codecForTipResponse(), ); - if (response.reserve_sigs.length !== tipRecord.planchets.length) { + if (response.blind_sigs.length !== tipRecord.planchets.length) { throw Error("number of tip responses does not match requested planchets"); } - const withdrawalGroupId = encodeCrock(getRandomBytes(32)); - const planchets: PlanchetRecord[] = []; - - for (let i = 0; i < tipRecord.planchets.length; i++) { - const tipPlanchet = tipRecord.planchets[i]; - const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv); - const planchet: PlanchetRecord = { - blindingKey: tipPlanchet.blindingKey, - coinEv: tipPlanchet.coinEv, - coinPriv: tipPlanchet.coinPriv, - coinPub: tipPlanchet.coinPub, - coinValue: tipPlanchet.coinValue, - denomPub: tipPlanchet.denomPub, - denomPubHash: tipPlanchet.denomPubHash, - reservePub: response.reserve_pub, - withdrawSig: response.reserve_sigs[i].reserve_sig, - isFromTip: true, - coinEvHash, - coinIdx: i, - withdrawalDone: false, - withdrawalGroupId: withdrawalGroupId, - lastError: undefined, - }; - planchets.push(planchet); - } + const newCoinRecords: CoinRecord[] = []; - const withdrawalGroup: WithdrawalGroupRecord = { - exchangeBaseUrl: tipRecord.exchangeUrl, - source: { - type: WithdrawalSourceType.Tip, - tipId: tipRecord.walletTipId, - }, - timestampStart: getTimestampNow(), - withdrawalGroupId: withdrawalGroupId, - rawWithdrawalAmount: tipRecord.amount, - retryInfo: initRetryInfo(), - timestampFinish: undefined, - lastError: undefined, - denomsSel: tipRecord.denomsSel, - }; + for (let i = 0; i < response.blind_sigs.length; i++) { + const blindedSig = response.blind_sigs[i].blind_sig; + + const planchet = tipRecord.planchets[i]; + + const denomSig = await ws.cryptoApi.rsaUnblind( + blindedSig, + planchet.blindingKey, + planchet.denomPub, + ); + + const isValid = await ws.cryptoApi.rsaVerify( + planchet.coinPub, + denomSig, + planchet.denomPub, + ); + + if (!isValid) { + await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => { + const tipRecord = await tx.get(Stores.tips, walletTipId); + if (!tipRecord) { + return; + } + tipRecord.lastError = makeErrorDetails( + TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID, + "invalid signature from the exchange (via merchant tip) after unblinding", + {}, + ); + await tx.put(Stores.tips, tipRecord); + }); + return; + } + + newCoinRecords.push({ + blindingKey: planchet.blindingKey, + coinPriv: planchet.coinPriv, + coinPub: planchet.coinPub, + coinSource: { + type: CoinSourceType.Tip, + coinIndex: i, + walletTipId: walletTipId, + }, + currentAmount: planchet.coinValue, + denomPub: planchet.denomPub, + denomPubHash: planchet.denomPubHash, + denomSig: denomSig, + exchangeBaseUrl: tipRecord.exchangeUrl, + status: CoinStatus.Fresh, + suspended: false, + }); + } await ws.db.runWithWriteTransaction( - [Stores.tips, Stores.withdrawalGroups], + [Stores.coins, Stores.tips, Stores.withdrawalGroups], async (tx) => { - const tr = await tx.get(Stores.tips, tipId); + const tr = await tx.get(Stores.tips, walletTipId); if (!tr) { return; } @@ -303,16 +320,12 @@ async function processTipImpl( } tr.pickedUp = true; tr.retryInfo = initRetryInfo(false); - await tx.put(Stores.tips, tr); - await tx.put(Stores.withdrawalGroups, withdrawalGroup); - for (const p of planchets) { - await tx.put(Stores.planchets, p); + for (const cr of newCoinRecords) { + await tx.put(Stores.coins, cr); } }, ); - - await processWithdrawGroup(ws, withdrawalGroupId); } export async function acceptTip( diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 7a3228422..b5f77a190 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -116,63 +116,49 @@ export async function getTransactions( return; } - switch (wsr.source.type) { - case WithdrawalSourceType.Reserve: - { - const r = await tx.get(Stores.reserves, wsr.source.reservePub); - if (!r) { - break; - } - let amountRaw: AmountJson | undefined = undefined; - if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) { - amountRaw = r.instructedAmount; - } else { - amountRaw = wsr.denomsSel.totalWithdrawCost; - } - let withdrawalDetails: WithdrawalDetails; - if (r.bankInfo) { - withdrawalDetails = { - type: WithdrawalType.TalerBankIntegrationApi, - confirmed: true, - bankConfirmationUrl: r.bankInfo.confirmUrl, - }; - } else { - const exchange = await tx.get( - Stores.exchanges, - r.exchangeBaseUrl, - ); - if (!exchange) { - // FIXME: report somehow - break; - } - withdrawalDetails = { - type: WithdrawalType.ManualTransfer, - exchangePaytoUris: - exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [], - }; - } - transactions.push({ - type: TransactionType.Withdrawal, - amountEffective: Amounts.stringify( - wsr.denomsSel.totalCoinValue, - ), - amountRaw: Amounts.stringify(amountRaw), - withdrawalDetails, - exchangeBaseUrl: wsr.exchangeBaseUrl, - pending: !wsr.timestampFinish, - timestamp: wsr.timestampStart, - transactionId: makeEventId( - TransactionType.Withdrawal, - wsr.withdrawalGroupId, - ), - ...(wsr.lastError ? { error: wsr.lastError } : {}), - }); - } - break; - default: - // Tips are reported via their own event - break; + const r = await tx.get(Stores.reserves, wsr.reservePub); + if (!r) { + return; + } + let amountRaw: AmountJson | undefined = undefined; + if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) { + amountRaw = r.instructedAmount; + } else { + amountRaw = wsr.denomsSel.totalWithdrawCost; + } + let withdrawalDetails: WithdrawalDetails; + if (r.bankInfo) { + withdrawalDetails = { + type: WithdrawalType.TalerBankIntegrationApi, + confirmed: true, + bankConfirmationUrl: r.bankInfo.confirmUrl, + }; + } else { + const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl); + if (!exchange) { + // FIXME: report somehow + return; + } + withdrawalDetails = { + type: WithdrawalType.ManualTransfer, + exchangePaytoUris: + exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [], + }; } + transactions.push({ + type: TransactionType.Withdrawal, + amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(amountRaw), + withdrawalDetails, + exchangeBaseUrl: wsr.exchangeBaseUrl, + pending: !wsr.timestampFinish, + timestamp: wsr.timestampStart, + transactionId: makeEventId( + TransactionType.Withdrawal, + wsr.withdrawalGroupId, + ), + ...(wsr.lastError ? { error: wsr.lastError } : {}), + }); }); // Report pending withdrawals based on reserves that diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 4070e39f4..eec92ba29 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -242,12 +242,9 @@ async function processPlanchetGenerate( if (!denom) { throw Error("invariant violated"); } - if (withdrawalGroup.source.type != WithdrawalSourceType.Reserve) { - throw Error("invariant violated"); - } const reserve = await ws.db.get( Stores.reserves, - withdrawalGroup.source.reservePub, + withdrawalGroup.reservePub, ); if (!reserve) { throw Error("invariant violated"); @@ -420,7 +417,7 @@ async function processPlanchetVerifyAndStoreCoin( if (!isValid) { await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => { - let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [ + let planchet = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [ withdrawalGroupId, coinIdx, ]); @@ -700,7 +697,7 @@ async function processWithdrawGroupImpl( if (finishedForFirstTime) { ws.notify({ type: NotificationType.WithdrawGroupFinished, - withdrawalSource: withdrawalGroup.source, + reservePub: withdrawalGroup.reservePub, }); } } diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts index 4e2ba1bb4..3e24f787b 100644 --- a/packages/taler-wallet-core/src/types/dbTypes.ts +++ b/packages/taler-wallet-core/src/types/dbTypes.ts @@ -694,17 +694,28 @@ export interface PlanchetRecord { lastError: TalerErrorDetails | undefined; /** - * Public key of the reserve, this might be a reserve not - * known to the wallet if the planchet is from a tip. + * Public key of the reserve that this planchet + * is being withdrawn from. + * + * Can be the empty string (non-null/undefined for DB indexing) + * if this is a tipping reserve. */ reservePub: string; + denomPubHash: string; + denomPub: string; + blindingKey: string; + withdrawSig: string; + coinEv: string; + coinEvHash: string; + coinValue: AmountJson; + isFromTip: boolean; } @@ -772,6 +783,8 @@ export interface RefreshCoinSource { export interface TipCoinSource { type: CoinSourceType.Tip; + walletTipId: string; + coinIndex: number; } export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource; @@ -950,9 +963,9 @@ export interface TipRecord { /** * The tipped amount. */ - amount: AmountJson; + tipAmountRaw: AmountJson; - totalFees: AmountJson; + tipAmountEffective: AmountJson; /** * Timestamp, the tip can't be picked up anymore after this deadline. @@ -1481,18 +1494,6 @@ export enum WithdrawalSourceType { Reserve = "reserve", } -export interface WithdrawalSourceTip { - type: WithdrawalSourceType.Tip; - tipId: string; -} - -export interface WithdrawalSourceReserve { - type: WithdrawalSourceType.Reserve; - reservePub: string; -} - -export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve; - export interface DenominationSelectionInfo { totalCoinValue: AmountJson; totalWithdrawCost: AmountJson; @@ -1524,12 +1525,7 @@ export interface DenomSelectionState { export interface WithdrawalGroupRecord { withdrawalGroupId: string; - /** - * Withdrawal source. Fields that don't apply to the respective - * withdrawal source type must be null (i.e. can't be absent), - * otherwise the IndexedDB indexing won't like us. - */ - source: WithdrawalSource; + reservePub: string; exchangeBaseUrl: string; diff --git a/packages/taler-wallet-core/src/types/notifications.ts b/packages/taler-wallet-core/src/types/notifications.ts index e1b9a7aff..d86c5ae59 100644 --- a/packages/taler-wallet-core/src/types/notifications.ts +++ b/packages/taler-wallet-core/src/types/notifications.ts @@ -23,7 +23,6 @@ * Imports. */ import { TalerErrorDetails } from "./walletTypes"; -import { WithdrawalSource } from "./dbTypes"; import { ReserveHistorySummary } from "../util/reserveHistoryUtil"; export enum NotificationType { @@ -141,7 +140,7 @@ export interface WithdrawalGroupCreatedNotification { export interface WithdrawalGroupFinishedNotification { type: NotificationType.WithdrawGroupFinished; - withdrawalSource: WithdrawalSource; + reservePub: string; } export interface WaitingForRetryNotification { diff --git a/packages/taler-wallet-core/src/types/pending.ts b/packages/taler-wallet-core/src/types/pending.ts index d07754fe9..b14872d74 100644 --- a/packages/taler-wallet-core/src/types/pending.ts +++ b/packages/taler-wallet-core/src/types/pending.ts @@ -22,7 +22,7 @@ * Imports. */ import { TalerErrorDetails, BalancesResponse } from "./walletTypes"; -import { WithdrawalSource, RetryInfo, ReserveRecordStatus } from "./dbTypes"; +import { RetryInfo, ReserveRecordStatus } from "./dbTypes"; import { Timestamp, Duration } from "../util/time"; export enum PendingOperationType { @@ -219,7 +219,6 @@ export interface PendingRecoupOperation { */ export interface PendingWithdrawOperation { type: PendingOperationType.Withdraw; - source: WithdrawalSource; lastError: TalerErrorDetails | undefined; retryInfo: RetryInfo; withdrawalGroupId: string; diff --git a/packages/taler-wallet-core/src/types/talerTypes.ts b/packages/taler-wallet-core/src/types/talerTypes.ts index 52dc4cb62..16d00e2ea 100644 --- a/packages/taler-wallet-core/src/types/talerTypes.ts +++ b/packages/taler-wallet-core/src/types/talerTypes.ts @@ -593,11 +593,11 @@ export interface TipPickupRequest { * Reserve signature, defined as separate class to facilitate * schema validation with "@Checkable". */ -export class ReserveSigSingleton { +export class BlindSigWrapper { /** * Reserve signature. */ - reserve_sig: string; + blind_sig: string; } /** @@ -606,14 +606,9 @@ export class ReserveSigSingleton { */ export class TipResponse { /** - * Public key of the reserve - */ - reserve_pub: string; - - /** * The order of the signatures matches the planchets list. */ - reserve_sigs: ReserveSigSingleton[]; + blind_sigs: BlindSigWrapper[]; } /** @@ -1166,15 +1161,14 @@ export const codecForMerchantRefundResponse = (): Codec< .property("refunds", codecForList(codecForMerchantRefundPermission())) .build("MerchantRefundResponse"); -export const codecForReserveSigSingleton = (): Codec<ReserveSigSingleton> => - buildCodecForObject<ReserveSigSingleton>() - .property("reserve_sig", codecForString()) - .build("ReserveSigSingleton"); +export const codecForBlindSigWrapper = (): Codec<BlindSigWrapper> => + buildCodecForObject<BlindSigWrapper>() + .property("blind_sig", codecForString()) + .build("BlindSigWrapper"); export const codecForTipResponse = (): Codec<TipResponse> => buildCodecForObject<TipResponse>() - .property("reserve_pub", codecForString()) - .property("reserve_sigs", codecForList(codecForReserveSigSingleton())) + .property("blind_sigs", codecForList(codecForBlindSigWrapper())) .build("TipResponse"); export const codecForRecoup = (): Codec<Recoup> => diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts index fb049caf9..c9014830b 100644 --- a/packages/taler-wallet-core/src/types/walletTypes.ts +++ b/packages/taler-wallet-core/src/types/walletTypes.ts @@ -359,8 +359,8 @@ export interface PrepareTipResult { * Has the tip already been accepted? */ accepted: boolean; - amount: AmountString; - totalFees: AmountString; + tipAmountRaw: AmountString; + tipAmountEffective: AmountString; exchangeBaseUrl: string; expirationTimestamp: Timestamp; } @@ -368,8 +368,8 @@ export interface PrepareTipResult { export const codecForPrepareTipResult = (): Codec<PrepareTipResult> => buildCodecForObject<PrepareTipResult>() .property("accepted", codecForBoolean()) - .property("amount", codecForAmountString()) - .property("totalFees", codecForAmountString()) + .property("tipAmountRaw", codecForAmountString()) + .property("tipAmountEffective", codecForAmountString()) .property("exchangeBaseUrl", codecForString()) .property("expirationTimestamp", codecForTimestamp) .property("walletTipId", codecForString()) diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 0507ac8b2..cd9646339 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -86,7 +86,6 @@ import { codecForPreparePayRequest, codecForIntegrationTestArgs, WithdrawTestBalanceRequest, - withdrawTestBalanceDefaults, codecForWithdrawTestBalance, codecForTestPayArgs, codecForSetCoinSuspendedRequest, @@ -916,9 +915,7 @@ export class Wallet { console.error("no withdrawal session found for coin"); continue; } - if (ws.source.type == "reserve") { - withdrawalReservePub = ws.source.reservePub; - } + withdrawalReservePub = ws.reservePub; } coinsJson.coins.push({ coin_pub: c.coinPub, |