diff options
12 files changed, 427 insertions, 179 deletions
diff --git a/packages/taler-wallet-core/src/crypto/talerCrypto.ts b/packages/taler-wallet-core/src/crypto/talerCrypto.ts index 4faa523a0..095957982 100644 --- a/packages/taler-wallet-core/src/crypto/talerCrypto.ts +++ b/packages/taler-wallet-core/src/crypto/talerCrypto.ts @@ -390,6 +390,25 @@ export function setupRefreshPlanchet( }; } +export function setupTipPlanchet( + secretSeed: Uint8Array, + coinNumber: number, +): FreshCoin { + const info = stringToBytes("taler-tip-coin-derivation"); + const saltArrBuf = new ArrayBuffer(4); + const salt = new Uint8Array(saltArrBuf); + const saltDataView = new DataView(saltArrBuf); + saltDataView.setUint32(0, coinNumber); + const out = kdf(64, secretSeed, salt, info); + const coinPriv = out.slice(0, 32); + const bks = out.slice(32, 64); + return { + bks, + coinPriv, + coinPub: eddsaGetPublic(coinPriv), + }; +} + export function setupRefreshTransferPub( secretSeed: Uint8Array, transferPubIndex: number, diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts index 6a4264d2c..ef149823c 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts @@ -22,16 +22,7 @@ /** * Imports. */ -import { AmountJson } from "../../util/amounts"; - -import { - CoinRecord, - DenominationRecord, - RefreshSessionRecord, - TipPlanchet, - WireFee, - DenominationSelectionInfo, -} from "../../types/dbTypes"; +import { CoinRecord, DenominationRecord, WireFee } from "../../types/dbTypes"; import { CryptoWorker } from "./cryptoWorker"; @@ -49,7 +40,9 @@ import * as timer from "../../util/timer"; import { Logger } from "../../util/logging"; import { DerivedRefreshSession, + DerivedTipPlanchet, DeriveRefreshSessionRequest, + DeriveTipRequest, } from "../../types/cryptoTypes"; const logger = new Logger("cryptoApi.ts"); @@ -329,8 +322,8 @@ export class CryptoApi { return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req); } - createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> { - return this.doRpc<TipPlanchet>("createTipPlanchet", 1, denom); + createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet> { + return this.doRpc<DerivedTipPlanchet>("createTipPlanchet", 1, req); } hashString(str: string): Promise<string> { diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts index d14f663e8..deaad42bb 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts @@ -30,11 +30,8 @@ import { CoinRecord, DenominationRecord, RefreshPlanchet, - RefreshSessionRecord, - TipPlanchet, WireFee, CoinSourceType, - DenominationSelectionInfo, } from "../../types/dbTypes"; import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes"; @@ -59,25 +56,25 @@ import { rsaUnblind, stringToBytes, createHashContext, - createEcdheKeyPair, keyExchangeEcdheEddsa, setupRefreshPlanchet, rsaVerify, - getRandomBytes, setupRefreshTransferPub, + setupTipPlanchet, } from "../talerCrypto"; import { randomBytes } from "../primitives/nacl-fast"; import { kdf } from "../primitives/kdf"; import { Timestamp, - getTimestampNow, timestampTruncateToSecond, } from "../../util/time"; import { Logger } from "../../util/logging"; import { DerivedRefreshSession, + DerivedTipPlanchet, DeriveRefreshSessionRequest, + DeriveTipRequest, } from "../../types/cryptoTypes"; const logger = new Logger("cryptoImplementation.ts"); @@ -199,21 +196,18 @@ export class CryptoImplementation { /** * Create a planchet used for tipping, including the private keys. */ - createTipPlanchet(denom: DenominationRecord): TipPlanchet { - const denomPub = decodeCrock(denom.denomPub); - const coinKeyPair = createEddsaKeyPair(); + createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet { + const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex); + const denomPub = decodeCrock(req.denomPub); const blindingFactor = createBlindingKeySecret(); - const coinPubHash = hash(coinKeyPair.eddsaPub); + const coinPubHash = hash(fc.coinPub); const ev = rsaBlind(coinPubHash, blindingFactor, denomPub); - const tipPlanchet: TipPlanchet = { + const tipPlanchet: DerivedTipPlanchet = { blindingKey: encodeCrock(blindingFactor), coinEv: encodeCrock(ev), - coinPriv: encodeCrock(coinKeyPair.eddsaPriv), - coinPub: encodeCrock(coinKeyPair.eddsaPub), - coinValue: denom.value, - denomPub: encodeCrock(denomPub), - denomPubHash: encodeCrock(hash(denomPub)), + coinPriv: encodeCrock(fc.coinPriv), + coinPub: encodeCrock(fc.coinPub), }; return tipPlanchet; } diff --git a/packages/taler-wallet-core/src/operations/backup.ts b/packages/taler-wallet-core/src/operations/backup.ts index f609d4354..7ab908c46 100644 --- a/packages/taler-wallet-core/src/operations/backup.ts +++ b/packages/taler-wallet-core/src/operations/backup.ts @@ -26,20 +26,34 @@ */ import { InternalWalletState } from "./state"; import { + BackupBackupProvider, BackupCoin, BackupCoinSource, BackupCoinSourceType, BackupDenomination, BackupExchange, BackupExchangeWireFee, + BackupProposal, + BackupProposalStatus, + BackupPurchase, + BackupRecoupGroup, + BackupRefreshGroup, + BackupRefreshOldCoin, + BackupRefreshSession, + BackupRefundItem, + BackupRefundState, BackupReserve, + BackupTip, WalletBackupContentV1, } from "../types/backupTypes"; import { TransactionHandle } from "../util/query"; import { + AbortStatus, CoinSourceType, CoinStatus, ConfigRecord, + ProposalStatus, + RefundState, Stores, } from "../types/dbTypes"; import { checkDbInvariant } from "../util/invariants"; @@ -56,7 +70,7 @@ import { import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers"; import { getTimestampNow, Timestamp } from "../util/time"; import { URL } from "../util/url"; -import { AmountString } from "../types/talerTypes"; +import { AmountString, TipResponse } from "../types/talerTypes"; import { buildCodecForObject, Codec, @@ -146,16 +160,80 @@ export async function exportBackup( ): Promise<WalletBackupContentV1> { await provideBackupState(ws); return ws.db.runWithWriteTransaction( - [Stores.config, Stores.exchanges, Stores.coins, Stores.denominations], + [ + Stores.config, + Stores.exchanges, + Stores.coins, + Stores.denominations, + Stores.purchases, + Stores.proposals, + Stores.refreshGroups, + Stores.backupProviders, + Stores.tips, + Stores.recoupGroups, + Stores.reserves, + ], async (tx) => { const bs = await getWalletBackupState(ws, tx); - const exchanges: BackupExchange[] = []; - const coinsByDenom: { [dph: string]: BackupCoin[] } = {}; - const denominationsByExchange: { + const backupExchanges: BackupExchange[] = []; + const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {}; + const backupDenominationsByExchange: { [url: string]: BackupDenomination[]; } = {}; - const reservesByExchange: { [url: string]: BackupReserve[] } = {}; + const backupReservesByExchange: { [url: string]: BackupReserve[] } = {}; + const backupPurchases: BackupPurchase[] = []; + const backupProposals: BackupProposal[] = []; + const backupRefreshGroups: BackupRefreshGroup[] = []; + const backupBackupProviders: BackupBackupProvider[] = []; + const backupTips: BackupTip[] = []; + const backupRecoupGroups: BackupRecoupGroup[] = []; + + await tx.iter(Stores.reserves).forEach((reserve) => { + // FIXME: implement + }); + + await tx.iter(Stores.tips).forEach((tip) => { + backupTips.push({ + exchange_base_url: tip.exchangeBaseUrl, + merchant_base_url: tip.merchantBaseUrl, + merchant_tip_id: tip.merchantTipId, + wallet_tip_id: tip.walletTipId, + secret_seed: tip.secretSeed, + selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({ + count: x.count, + denom_pub_hash: x.denomPubHash, + })), + timestam_picked_up: tip.pickedUpTimestamp, + timestamp_accepted: tip.acceptedTimestamp, + timestamp_created: tip.createdTimestamp, + timestamp_expiration: tip.tipExpiration, + tip_amount_raw: Amounts.stringify(tip.tipAmountRaw), + }); + }); + + await tx.iter(Stores.recoupGroups).forEach((recoupGroup) => { + backupRecoupGroups.push({ + recoup_group_id: recoupGroup.recoupGroupId, + timestamp_started: recoupGroup.timestampStarted, + timestamp_finished: recoupGroup.timestampFinished, + coins: recoupGroup.coinPubs.map((x, i) => ({ + coin_pub: x, + recoup_finished: recoupGroup.recoupFinishedPerCoin[i], + old_amount: Amounts.stringify(recoupGroup.oldAmountPerCoin[i]), + })), + }); + }); + + await tx.iter(Stores.backupProviders).forEach((bp) => { + backupBackupProviders.push({ + annual_fee: Amounts.stringify(bp.annualFee), + base_url: canonicalizeBaseUrl(bp.baseUrl), + pay_proposal_ids: [], + storage_limit_in_megabytes: bp.storageLimitInMegabytes, + supported_protocol_version: bp.supportedProtocolVersion, + }); + }); await tx.iter(Stores.coins).forEach((coin) => { let bcs: BackupCoinSource; @@ -183,7 +261,7 @@ export async function exportBackup( break; } - const coins = (coinsByDenom[coin.denomPubHash] ??= []); + const coins = (backupCoinsByDenom[coin.denomPubHash] ??= []); coins.push({ blinding_key: coin.blindingKey, coin_priv: coin.coinPriv, @@ -195,11 +273,11 @@ export async function exportBackup( }); await tx.iter(Stores.denominations).forEach((denom) => { - const backupDenoms = (denominationsByExchange[ + const backupDenoms = (backupDenominationsByExchange[ denom.exchangeBaseUrl ] ??= []); backupDenoms.push({ - coins: coinsByDenom[denom.denomPubHash] ?? [], + coins: backupCoinsByDenom[denom.denomPubHash] ?? [], denom_pub: denom.denomPub, fee_deposit: Amounts.stringify(denom.feeDeposit), fee_refresh: Amounts.stringify(denom.feeRefresh), @@ -247,7 +325,7 @@ export async function exportBackup( } }); - exchanges.push({ + backupExchanges.push({ base_url: ex.baseUrl, accounts: ex.wireInfo.accounts.map((x) => ({ payto_uri: x.payto_uri, @@ -271,8 +349,132 @@ export async function exportBackup( })), tos_etag_accepted: ex.termsOfServiceAcceptedEtag, tos_etag_last: ex.termsOfServiceLastEtag, - denominations: denominationsByExchange[ex.baseUrl] ?? [], - reserves: reservesByExchange[ex.baseUrl] ?? [], + denominations: backupDenominationsByExchange[ex.baseUrl] ?? [], + reserves: backupReservesByExchange[ex.baseUrl] ?? [], + }); + }); + + const purchaseProposalIdSet = new Set<string>(); + + await tx.iter(Stores.purchases).forEach((purch) => { + const refunds: BackupRefundItem[] = []; + purchaseProposalIdSet.add(purch.proposalId); + for (const refundKey of Object.keys(purch.refunds)) { + const ri = purch.refunds[refundKey]; + const common = { + coin_pub: ri.coinPub, + execution_time: ri.executionTime, + obtained_time: ri.obtainedTime, + refund_amount: Amounts.stringify(ri.refundAmount), + rtransaction_id: ri.rtransactionId, + total_refresh_cost_bound: Amounts.stringify( + ri.totalRefreshCostBound, + ), + }; + switch (ri.type) { + case RefundState.Applied: + refunds.push({ type: BackupRefundState.Applied, ...common }); + break; + case RefundState.Failed: + refunds.push({ type: BackupRefundState.Failed, ...common }); + break; + case RefundState.Pending: + refunds.push({ type: BackupRefundState.Pending, ...common }); + break; + } + } + + backupPurchases.push({ + clock_created: 1, + contract_terms_raw: purch.contractTermsRaw, + auto_refund_deadline: purch.autoRefundDeadline, + merchant_pay_sig: purch.merchantPaySig, + pay_coins: purch.payCoinSelection.coinPubs.map((x, i) => ({ + coin_pub: x, + contribution: Amounts.stringify( + purch.payCoinSelection.coinContributions[i], + ), + })), + proposal_id: purch.proposalId, + refunds, + timestamp_accept: purch.timestampAccept, + timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay, + timestamp_last_refund_status: purch.timestampLastRefundStatus, + abort_status: + purch.abortStatus === AbortStatus.None + ? undefined + : purch.abortStatus, + nonce_priv: purch.noncePriv, + }); + }); + + await tx.iter(Stores.proposals).forEach((prop) => { + if (purchaseProposalIdSet.has(prop.proposalId)) { + return; + } + let propStatus: BackupProposalStatus; + switch (prop.proposalStatus) { + case ProposalStatus.ACCEPTED: + return; + case ProposalStatus.DOWNLOADING: + case ProposalStatus.PROPOSED: + propStatus = BackupProposalStatus.Proposed; + break; + case ProposalStatus.PERMANENTLY_FAILED: + propStatus = BackupProposalStatus.PermanentlyFailed; + break; + case ProposalStatus.REFUSED: + propStatus = BackupProposalStatus.Refused; + break; + case ProposalStatus.REPURCHASE: + propStatus = BackupProposalStatus.Repurchase; + break; + } + backupProposals.push({ + claim_token: prop.claimToken, + nonce_priv: prop.noncePriv, + proposal_id: prop.noncePriv, + proposal_status: propStatus, + repurchase_proposal_id: prop.repurchaseProposalId, + timestamp: prop.timestamp, + contract_terms_raw: prop.download?.contractTermsRaw, + download_session_id: prop.downloadSessionId, + }); + }); + + await tx.iter(Stores.refreshGroups).forEach((rg) => { + const oldCoins: BackupRefreshOldCoin[] = []; + + for (let i = 0; i < rg.oldCoinPubs.length; i++) { + let refreshSession: BackupRefreshSession | undefined; + const s = rg.refreshSessionPerCoin[i]; + if (s) { + refreshSession = { + new_denoms: s.newDenoms.map((x) => ({ + count: x.count, + denom_pub_hash: x.denomPubHash, + })), + session_secret_seed: s.sessionSecretSeed, + noreveal_index: s.norevealIndex, + }; + } + oldCoins.push({ + coin_pub: rg.oldCoinPubs[i], + estimated_output_amount: Amounts.stringify( + rg.estimatedOutputPerCoin[i], + ), + finished: rg.finishedPerCoin[i], + input_amount: Amounts.stringify(rg.inputPerCoin[i]), + refresh_session: refreshSession, + }); + } + + backupRefreshGroups.push({ + reason: rg.reason as any, + refresh_group_id: rg.refreshGroupId, + timestamp_started: rg.timestampCreated, + timestamp_finished: rg.timestampFinished, + old_coins: oldCoins, }); }); @@ -284,16 +486,16 @@ export async function exportBackup( schema_id: "gnu-taler-wallet-backup-content", schema_version: 1, clocks: bs.clocks, - exchanges: exchanges, + exchanges: backupExchanges, wallet_root_pub: bs.walletRootPub, - backup_providers: [], + backup_providers: backupBackupProviders, current_device_id: bs.deviceId, - proposals: [], + proposals: backupProposals, purchase_tombstones: [], - purchases: [], - recoup_groups: [], - refresh_groups: [], - tips: [], + purchases: backupPurchases, + recoup_groups: backupRecoupGroups, + refresh_groups: backupRefreshGroups, + tips: backupTips, timestamp: bs.lastBackupTimestamp, trusted_auditors: {}, trusted_exchanges: {}, diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 16b691ec8..71cc78fa9 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -711,6 +711,7 @@ export async function createRefreshGroup( retryInfo: initRetryInfo(), inputPerCoin, estimatedOutputPerCoin, + timestampCreated: getTimestampNow(), }; if (oldCoinPubs.length == 0) { diff --git a/packages/taler-wallet-core/src/operations/refund.ts b/packages/taler-wallet-core/src/operations/refund.ts index 36b21b232..367b644a2 100644 --- a/packages/taler-wallet-core/src/operations/refund.ts +++ b/packages/taler-wallet-core/src/operations/refund.ts @@ -158,6 +158,8 @@ async function applySuccessfulRefund( refundAmount: Amounts.parseOrThrow(r.refund_amount), refundFee: denom.feeRefund, totalRefreshCostBound, + coinPub: r.coin_pub, + rtransactionId: r.rtransaction_id, }; } @@ -208,6 +210,8 @@ async function storePendingRefund( refundAmount: Amounts.parseOrThrow(r.refund_amount), refundFee: denom.feeRefund, totalRefreshCostBound, + coinPub: r.coin_pub, + rtransactionId: r.rtransaction_id, }; } @@ -259,6 +263,8 @@ async function storeFailedRefund( refundAmount: Amounts.parseOrThrow(r.refund_amount), refundFee: denom.feeRefund, totalRefreshCostBound, + coinPub: r.coin_pub, + rtransactionId: r.rtransaction_id, }; if (p.abortStatus === AbortStatus.AbortRefund) { diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index a57963824..bc10e346d 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -25,10 +25,10 @@ import { import * as Amounts from "../util/amounts"; import { Stores, - TipPlanchet, CoinRecord, CoinSourceType, CoinStatus, + DenominationRecord, } from "../types/dbTypes"; import { getExchangeWithdrawalInfo, @@ -50,6 +50,7 @@ import { checkDbInvariant } from "../util/invariants"; import { TalerErrorCode } from "../TalerErrorCode"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries"; import { j2s } from "../util/helpers"; +import { DerivedTipPlanchet } from '../types/cryptoTypes'; const logger = new Logger("operations/tip.ts"); @@ -201,39 +202,34 @@ async function processTipImpl( const denomsForWithdraw = tipRecord.denomsSel; - if (!tipRecord.planchets) { - const planchets: TipPlanchet[] = []; - - for (const sd of denomsForWithdraw.selectedDenoms) { - const denom = await ws.db.get(Stores.denominations, [ - tipRecord.exchangeBaseUrl, - sd.denomPubHash, - ]); - if (!denom) { - throw Error("denom does not exist anymore"); - } - for (let i = 0; i < sd.count; i++) { - const r = await ws.cryptoApi.createTipPlanchet(denom); - planchets.push(r); - } - } - await ws.db.mutate(Stores.tips, walletTipId, (r) => { - if (!r.planchets) { - r.planchets = planchets; - } - return r; - }); - } - tipRecord = await ws.db.get(Stores.tips, walletTipId); checkDbInvariant(!!tipRecord, "tip record should be in database"); - checkDbInvariant(!!tipRecord.planchets, "tip record should have planchets"); + const planchets: DerivedTipPlanchet[] = []; // Planchets in the form that the merchant expects - const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({ - coin_ev: p.coinEv, - denom_pub_hash: p.denomPubHash, - })); + const planchetsDetail: TipPlanchetDetail[] = []; + const denomForPlanchet: { [index: number]: DenominationRecord} = []; + + for (const dh of denomsForWithdraw.selectedDenoms) { + const denom = await ws.db.get(Stores.denominations, [ + tipRecord.exchangeBaseUrl, + dh.denomPubHash, + ]); + checkDbInvariant(!!denom, "denomination should be in database"); + denomForPlanchet[planchets.length] = denom; + for (let i = 0; i < dh.count; i++) { + const p = await ws.cryptoApi.createTipPlanchet({ + denomPub: dh.denomPubHash, + planchetIndex: planchets.length, + secretSeed: tipRecord.secretSeed, + }); + planchets.push(p); + planchetsDetail.push({ + coin_ev: p.coinEv, + denom_pub_hash: denom.denomPubHash, + }); + } + } const tipStatusUrl = new URL( `tips/${tipRecord.merchantTipId}/pickup`, @@ -264,7 +260,7 @@ async function processTipImpl( codecForTipResponse(), ); - if (response.blind_sigs.length !== tipRecord.planchets.length) { + if (response.blind_sigs.length !== planchets.length) { throw Error("number of tip responses does not match requested planchets"); } @@ -273,18 +269,19 @@ async function processTipImpl( for (let i = 0; i < response.blind_sigs.length; i++) { const blindedSig = response.blind_sigs[i].blind_sig; - const planchet = tipRecord.planchets[i]; + const denom = denomForPlanchet[i]; + const planchet = planchets[i]; const denomSig = await ws.cryptoApi.rsaUnblind( blindedSig, planchet.blindingKey, - planchet.denomPub, + denom.denomPub, ); const isValid = await ws.cryptoApi.rsaVerify( planchet.coinPub, denomSig, - planchet.denomPub, + denom.denomPub, ); if (!isValid) { @@ -312,9 +309,9 @@ async function processTipImpl( coinIndex: i, walletTipId: walletTipId, }, - currentAmount: planchet.coinValue, - denomPub: planchet.denomPub, - denomPubHash: planchet.denomPubHash, + currentAmount: denom.value, + denomPub: denom.denomPub, + denomPubHash: denom.denomPubHash, denomSig: denomSig, exchangeBaseUrl: tipRecord.exchangeBaseUrl, status: CoinStatus.Fresh, diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index e4cbdd8c3..56e07a426 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -92,7 +92,6 @@ export async function getTransactions( Stores.purchases, Stores.refreshGroups, Stores.reserves, - Stores.reserveHistory, Stores.tips, Stores.withdrawalGroups, Stores.planchets, diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts b/packages/taler-wallet-core/src/types/backupTypes.ts index daf2fbe5a..09bc4670c 100644 --- a/packages/taler-wallet-core/src/types/backupTypes.ts +++ b/packages/taler-wallet-core/src/types/backupTypes.ts @@ -24,6 +24,20 @@ * 1. Exchange/auditor trust isn't exported yet * (see https://bugs.gnunet.org/view.php?id=6448) * 2. Reports to the auditor (cryptographic proofs and/or diagnostics) aren't exported yet + * 3. "Ghost spends", where a coin is spent unexpectedly by another wallet + * and a corresponding transaction (that is missing some details!) should + * be added to the transaction history, aren't implemented yet. + * 4. Clocks for denom/coin selections aren't properly modeled yet. + * (Needed for re-denomination of withdrawal / re-selection of coins) + * 5. Preferences about how currencies are to be displayed + * aren't exported yet (and not even implemented in wallet-core). + * 6. Returning money to own bank account isn't supported/exported yet. + * 7. Peer-to-peer payments aren't supported yet. + * + * Questions: + * 1. What happens when two backups are merged that have + * the same coin in different refresh groups? + * => Both are added, one will eventually fail * * General considerations / decisions: * 1. Information about previously occurring errors and @@ -318,6 +332,9 @@ export interface BackupRecoupGroup { /** * Timestamp when the recoup finished. + * + * (That means all coins have been recouped and coins to + * be refreshed have been put in a refresh group.) */ timestamp_finished: Timestamp | undefined; @@ -326,15 +343,9 @@ export interface BackupRecoupGroup { */ coins: { coin_pub: string; - finished: boolean; + recoup_finished: boolean; old_amount: BackupAmountString; }[]; - - /** - * Public keys of coins that should be scheduled for refreshing - * after all individual recoups are done. - */ - recoup_refresh_coins: string[]; } /** @@ -466,6 +477,11 @@ export interface BackupTip { merchant_tip_id: string; /** + * Secret seed used for the tipping planchets. + */ + secret_seed: string; + + /** * Has the user accepted the tip? Only after the tip has been accepted coins * withdrawn from the tip may be used. */ @@ -503,15 +519,6 @@ export interface BackupTip { merchant_base_url: string; /** - * Planchets, the members included in TipPlanchetDetail will be sent to the - * merchant. - */ - planchets?: { - blinding_key: string; - coin_priv: string; - }[]; - - /** * Selected denominations. Determines the effective tip amount. */ selected_denoms: { @@ -543,7 +550,10 @@ export interface BackupRefreshSession { /** * Hased denominations of the newly requested coins. */ - new_denom_hashes: string[]; + new_denoms: { + count: number; + denom_pub_hash: string; + }[]; /** * Seed used to derive the planchets and @@ -558,6 +568,39 @@ export interface BackupRefreshSession { } /** + * Refresh session for one coin inside a refresh group. + */ +export interface BackupRefreshOldCoin { + /** + * Public key of the old coin, + */ + coin_pub: string; + + /** + * Requested amount to refresh. Must be subtracted from the coin's remaining + * amount as soon as the coin is added to the refresh group. + */ + input_amount: BackupAmountString; + + /** + * Estimated output (may change if it takes a long time to create the + * actual session). + */ + estimated_output_amount: BackupAmountString; + + /** + * Did the refresh session finish (or was it unnecessary/impossible to create + * one) + */ + finished: boolean; + + /** + * Refresh session (if created) or undefined it not created yet. + */ + refresh_session: BackupRefreshSession | undefined; +} + +/** * Information about one refresh group. * * May span more than one exchange, but typically doesn't @@ -570,35 +613,9 @@ export interface BackupRefreshGroup { /** * Details per old coin. */ - old_coins: { - /** - * Public key of the old coin, - */ - coin_pub: string; - - /** - * Requested amount to refresh. Must be subtracted from the coin's remaining - * amount as soon as the coin is added to the refresh group. - */ - input_amount: BackupAmountString; - - /** - * Estimated output (may change if it takes a long time to create the - * actual session). - */ - estimated_output_amount: BackupAmountString; + old_coins: BackupRefreshOldCoin[]; - /** - * Did the refresh session finish (or was it unnecessary/impossible to create - * one) - */ - finished: boolean; - - /** - * Refresh session (if created) or undefined it not created yet. - */ - refresh_session: BackupRefreshSession | undefined; - }[]; + timestamp_started: Timestamp; /** * Timestamp when the refresh group finished. @@ -741,22 +758,23 @@ export interface BackupPurchase { */ contract_terms_raw: string; + /** + * Private key for the nonce. Might eventually be used + * to prove ownership of the contract. + */ + nonce_priv: string; + pay_coins: { /** * Public keys of the coins that were selected. */ - coin_pubs: string[]; - - /** - * Deposit permission signature of each coin. - */ - coin_sigs: string[]; + coin_pub: string; /** * Amount that each coin contributes. */ contribution: BackupAmountString; - }; + }[]; /** * Timestamp of the first time that sending a payment to the merchant @@ -1132,6 +1150,9 @@ export interface BackupReserveHistoryCreditItem { matched_exchange_transaction?: ReserveCreditTransaction; } +/** + * Reserve history item for a withdrawal + */ export interface BackupReserveHistoryWithdrawItem { type: WalletReserveHistoryItemType.Withdraw; @@ -1141,7 +1162,7 @@ export interface BackupReserveHistoryWithdrawItem { * Hash of the blinded coin. * * When this value is set, it indicates that a withdrawal is active - * in the wallet for the + * in the wallet for the reserve. */ expected_coin_ev_hash?: string; @@ -1183,13 +1204,11 @@ export type BackupReserveHistoryItem = | BackupReserveHistoryRecoupItem | BackupReserveHistoryClosingItem; -export enum ProposalStatus { - /** - * Not downloaded yet. - */ - Downloading = "downloading", +export enum BackupProposalStatus { /** - * Proposal downloaded, but the user needs to accept/reject it. + * Proposed (and either downloaded or not, + * depending on whether contract terms are present), + * but the user needs to accept/reject it. */ Proposed = "proposed", /** @@ -1202,6 +1221,8 @@ export enum ProposalStatus { Refused = "refused", /** * Downloading or processing the proposal has failed permanently. + * + * FIXME: Should this be modeled as a "misbehavior report" instead? */ PermanentlyFailed = "permanently-failed", /** @@ -1236,11 +1257,6 @@ export interface BackupProposal { nonce_priv: string; /** - * Public key for the nonce. - */ - nonce_pub: string; - - /** * Claim token initially given by the merchant. */ claim_token: string | undefined; @@ -1248,7 +1264,7 @@ export interface BackupProposal { /** * Status of the proposal. */ - proposal_status: ProposalStatus; + proposal_status: BackupProposalStatus; /** * Proposal that this one got "redirected" to as part of diff --git a/packages/taler-wallet-core/src/types/cryptoTypes.ts b/packages/taler-wallet-core/src/types/cryptoTypes.ts index a7f51ab52..98bdf92ec 100644 --- a/packages/taler-wallet-core/src/types/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/types/cryptoTypes.ts @@ -109,3 +109,19 @@ export interface DerivedRefreshSession { */ meltValueWithFee: AmountJson; } + +export interface DeriveTipRequest { + secretSeed: string; + denomPub: string; + planchetIndex: number; +} + +/** + * Tipping planchet stored in the database. + */ +export interface DerivedTipPlanchet { + blindingKey: string; + coinEv: string; + coinPriv: string; + coinPub: string; +} diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts index 18a1102b4..3a42b8dbc 100644 --- a/packages/taler-wallet-core/src/types/dbTypes.ts +++ b/packages/taler-wallet-core/src/types/dbTypes.ts @@ -109,14 +109,6 @@ export interface WalletReserveHistoryCreditItem { export interface WalletReserveHistoryWithdrawItem { expectedAmount?: AmountJson; - /** - * Hash of the blinded coin. - * - * When this value is set, it indicates that a withdrawal is active - * in the wallet for the - */ - expectedCoinEvHash?: string; - type: WalletReserveHistoryItemType.Withdraw; /** @@ -921,11 +913,9 @@ export interface TipRecord { merchantBaseUrl: string; /** - * Planchets, the members included in TipPlanchetDetail will be sent to the - * merchant. + * Denomination selection made by the wallet for picking up + * this tip. */ - planchets?: TipPlanchet[]; - denomsSel: DenomSelectionState; /** @@ -934,6 +924,11 @@ export interface TipRecord { walletTipId: string; /** + * Secret seed used to derive planchets for this tip. + */ + secretSeed: string; + + /** * The merchant's identifier for this tip. */ merchantTipId: string; @@ -984,6 +979,8 @@ export interface RefreshGroupRecord { */ finishedPerCoin: boolean[]; + timestampCreated: Timestamp; + /** * Timestamp when the refresh session finished. */ @@ -1024,19 +1021,6 @@ export interface RefreshSessionRecord { } /** - * Tipping planchet stored in the database. - */ -export interface TipPlanchet { - blindingKey: string; - coinEv: string; - coinPriv: string; - coinPub: string; - coinValue: AmountJson; - denomPubHash: string; - denomPub: string; -} - -/** * Wire fee for one wire method as stored in the * wallet's database. */ @@ -1106,6 +1090,7 @@ export interface WalletRefundItemCommon { obtainedTime: Timestamp; refundAmount: AmountJson; + refundFee: AmountJson; /** @@ -1116,6 +1101,10 @@ export interface WalletRefundItemCommon { * coin are refreshed in the same refresh operation. */ totalRefreshCostBound: AmountJson; + + coinPub: string; + + rtransactionId: number; } /** @@ -1267,10 +1256,23 @@ export interface PurchaseRecord { proposalId: string; /** + * Private key for the nonce. + */ + noncePriv: string; + + /** + * Public key for the nonce. + */ + noncePub: string; + + /** * Contract terms we got from the merchant. */ contractTermsRaw: string; + /** + * Parsed contract terms. + */ contractData: WalletContractData; /** diff --git a/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts b/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts index 855b71a3d..60823e1e0 100644 --- a/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts +++ b/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts @@ -15,6 +15,12 @@ */ /** + * Helpers for dealing with reserve histories. + * + * @author Florian Dold <dold@taler.net> + */ + +/** * Imports. */ import { @@ -31,11 +37,8 @@ import { deepCopy } from "./helpers"; import { AmountJson } from "../util/amounts"; /** - * Helpers for dealing with reserve histories. - * - * @author Florian Dold <dold@taler.net> + * Result of a reserve reconciliation. */ - export interface ReserveReconciliationResult { /** * The wallet's local history reconciled with the exchange's reserve history. |