aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-wallet-core/src/crypto/talerCrypto.ts19
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts17
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts26
-rw-r--r--packages/taler-wallet-core/src/operations/backup.ts240
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/refund.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts71
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts1
-rw-r--r--packages/taler-wallet-core/src/types/backupTypes.ts146
-rw-r--r--packages/taler-wallet-core/src/types/cryptoTypes.ts16
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.ts52
-rw-r--r--packages/taler-wallet-core/src/util/reserveHistoryUtil.ts11
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.