wallet-core: remove old sync code, add stored backups skeleton
This commit is contained in:
parent
0a4782a0da
commit
a713d90c3c
File diff suppressed because it is too large
Load Diff
@ -2655,3 +2655,21 @@ export interface TransactionRecordFilter {
|
||||
onlyState?: TransactionStateFilter;
|
||||
onlyCurrency?: string;
|
||||
}
|
||||
|
||||
export interface StoredBackupList {
|
||||
storedBackups: {
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface CreateStoredBackupResponse {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface RecoverStoredBackupRequest {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DeleteStoredBackupRequest {
|
||||
name: string;
|
||||
}
|
||||
|
@ -2769,6 +2769,24 @@ export const walletMetadataStore = {
|
||||
),
|
||||
};
|
||||
|
||||
export interface StoredBackupMeta {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface StoredBackupData {
|
||||
name: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export const StoredBackupStores = {
|
||||
backupMeta: describeStore(
|
||||
"backupMeta",
|
||||
describeContents<MetaConfigRecord>({ keyPath: "name" }),
|
||||
{},
|
||||
),
|
||||
backupData: describeStore("backupData", describeContents<any>({}), {}),
|
||||
};
|
||||
|
||||
export interface DbDumpRecord {
|
||||
/**
|
||||
* Key, serialized with structuredEncapsulated.
|
||||
@ -2831,6 +2849,7 @@ export async function exportSingleDb(
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = myDb.transaction(Array.from(myDb.objectStoreNames));
|
||||
tx.addEventListener("complete", () => {
|
||||
myDb.close();
|
||||
resolve(singleDbDump);
|
||||
});
|
||||
// tslint:disable-next-line:prefer-for-of
|
||||
@ -3211,6 +3230,36 @@ function onMetaDbUpgradeNeeded(
|
||||
);
|
||||
}
|
||||
|
||||
function onStoredBackupsDbUpgradeNeeded(
|
||||
db: IDBDatabase,
|
||||
oldVersion: number,
|
||||
newVersion: number,
|
||||
upgradeTransaction: IDBTransaction,
|
||||
) {
|
||||
upgradeFromStoreMap(
|
||||
StoredBackupStores,
|
||||
db,
|
||||
oldVersion,
|
||||
newVersion,
|
||||
upgradeTransaction,
|
||||
);
|
||||
}
|
||||
|
||||
export async function openStoredBackupsDatabase(
|
||||
idbFactory: IDBFactory,
|
||||
): Promise<DbAccess<typeof StoredBackupStores>> {
|
||||
const backupsDbHandle = await openDatabase(
|
||||
idbFactory,
|
||||
TALER_WALLET_META_DB_NAME,
|
||||
1,
|
||||
() => {},
|
||||
onStoredBackupsDbUpgradeNeeded,
|
||||
);
|
||||
|
||||
const handle = new DbAccess(backupsDbHandle, StoredBackupStores);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that resolves
|
||||
* to the taler wallet db.
|
||||
|
@ -1,586 +0,0 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2020 Taler Systems SA
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implementation of wallet backups (export/import/upload) and sync
|
||||
* server management.
|
||||
*
|
||||
* @author Florian Dold <dold@taler.net>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import {
|
||||
AbsoluteTime,
|
||||
Amounts,
|
||||
BackupBackupProvider,
|
||||
BackupBackupProviderTerms,
|
||||
BackupCoin,
|
||||
BackupCoinSource,
|
||||
BackupCoinSourceType,
|
||||
BackupDenomination,
|
||||
BackupExchange,
|
||||
BackupExchangeDetails,
|
||||
BackupExchangeSignKey,
|
||||
BackupExchangeWireFee,
|
||||
BackupOperationStatus,
|
||||
BackupPayInfo,
|
||||
BackupProposalStatus,
|
||||
BackupPurchase,
|
||||
BackupRecoupGroup,
|
||||
BackupRefreshGroup,
|
||||
BackupRefreshOldCoin,
|
||||
BackupRefreshSession,
|
||||
BackupRefundItem,
|
||||
BackupRefundState,
|
||||
BackupTip,
|
||||
BackupWgInfo,
|
||||
BackupWgType,
|
||||
BackupWithdrawalGroup,
|
||||
BACKUP_VERSION_MAJOR,
|
||||
BACKUP_VERSION_MINOR,
|
||||
canonicalizeBaseUrl,
|
||||
canonicalJson,
|
||||
CoinStatus,
|
||||
encodeCrock,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
Logger,
|
||||
stringToBytes,
|
||||
WalletBackupContentV1,
|
||||
TalerPreciseTimestamp,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
CoinSourceType,
|
||||
ConfigRecordKey,
|
||||
DenominationRecord,
|
||||
PurchaseStatus,
|
||||
RefreshCoinStatus,
|
||||
WithdrawalGroupStatus,
|
||||
WithdrawalRecordType,
|
||||
} from "../../db.js";
|
||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
||||
import { checkDbInvariant } from "../../util/invariants.js";
|
||||
import { getWalletBackupState, provideBackupState } from "./state.js";
|
||||
|
||||
const logger = new Logger("backup/export.ts");
|
||||
|
||||
export async function exportBackup(
|
||||
ws: InternalWalletState,
|
||||
): Promise<WalletBackupContentV1> {
|
||||
await provideBackupState(ws);
|
||||
return ws.db
|
||||
.mktx((x) => [
|
||||
x.config,
|
||||
x.exchanges,
|
||||
x.exchangeDetails,
|
||||
x.exchangeSignKeys,
|
||||
x.coins,
|
||||
x.contractTerms,
|
||||
x.denominations,
|
||||
x.purchases,
|
||||
x.refreshGroups,
|
||||
x.backupProviders,
|
||||
x.rewards,
|
||||
x.recoupGroups,
|
||||
x.withdrawalGroups,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const bs = await getWalletBackupState(ws, tx);
|
||||
|
||||
const backupExchangeDetails: BackupExchangeDetails[] = [];
|
||||
const backupExchanges: BackupExchange[] = [];
|
||||
const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {};
|
||||
const backupDenominationsByExchange: {
|
||||
[url: string]: BackupDenomination[];
|
||||
} = {};
|
||||
const backupPurchases: BackupPurchase[] = [];
|
||||
const backupRefreshGroups: BackupRefreshGroup[] = [];
|
||||
const backupBackupProviders: BackupBackupProvider[] = [];
|
||||
const backupTips: BackupTip[] = [];
|
||||
const backupRecoupGroups: BackupRecoupGroup[] = [];
|
||||
const backupWithdrawalGroups: BackupWithdrawalGroup[] = [];
|
||||
|
||||
await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
|
||||
let info: BackupWgInfo;
|
||||
switch (wg.wgInfo.withdrawalType) {
|
||||
case WithdrawalRecordType.BankIntegrated:
|
||||
info = {
|
||||
type: BackupWgType.BankIntegrated,
|
||||
exchange_payto_uri: wg.wgInfo.bankInfo.exchangePaytoUri,
|
||||
taler_withdraw_uri: wg.wgInfo.bankInfo.talerWithdrawUri,
|
||||
confirm_url: wg.wgInfo.bankInfo.confirmUrl,
|
||||
timestamp_bank_confirmed:
|
||||
wg.wgInfo.bankInfo.timestampBankConfirmed,
|
||||
timestamp_reserve_info_posted:
|
||||
wg.wgInfo.bankInfo.timestampReserveInfoPosted,
|
||||
};
|
||||
break;
|
||||
case WithdrawalRecordType.BankManual:
|
||||
info = {
|
||||
type: BackupWgType.BankManual,
|
||||
};
|
||||
break;
|
||||
case WithdrawalRecordType.PeerPullCredit:
|
||||
info = {
|
||||
type: BackupWgType.PeerPullCredit,
|
||||
contract_priv: wg.wgInfo.contractPriv,
|
||||
contract_terms: wg.wgInfo.contractTerms,
|
||||
};
|
||||
break;
|
||||
case WithdrawalRecordType.PeerPushCredit:
|
||||
info = {
|
||||
type: BackupWgType.PeerPushCredit,
|
||||
contract_terms: wg.wgInfo.contractTerms,
|
||||
};
|
||||
break;
|
||||
case WithdrawalRecordType.Recoup:
|
||||
info = {
|
||||
type: BackupWgType.Recoup,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(wg.wgInfo);
|
||||
}
|
||||
backupWithdrawalGroups.push({
|
||||
raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount),
|
||||
info,
|
||||
timestamp_created: wg.timestampStart,
|
||||
timestamp_finish: wg.timestampFinish,
|
||||
withdrawal_group_id: wg.withdrawalGroupId,
|
||||
secret_seed: wg.secretSeed,
|
||||
exchange_base_url: wg.exchangeBaseUrl,
|
||||
instructed_amount: Amounts.stringify(wg.instructedAmount),
|
||||
effective_withdrawal_amount: Amounts.stringify(
|
||||
wg.effectiveWithdrawalAmount,
|
||||
),
|
||||
reserve_priv: wg.reservePriv,
|
||||
restrict_age: wg.restrictAge,
|
||||
// FIXME: proper status conversion!
|
||||
operation_status:
|
||||
wg.status == WithdrawalGroupStatus.Finished
|
||||
? BackupOperationStatus.Finished
|
||||
: BackupOperationStatus.Pending,
|
||||
selected_denoms_uid: wg.denomSelUid,
|
||||
selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({
|
||||
count: x.count,
|
||||
denom_pub_hash: x.denomPubHash,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
await tx.rewards.iter().forEach((tip) => {
|
||||
backupTips.push({
|
||||
exchange_base_url: tip.exchangeBaseUrl,
|
||||
merchant_base_url: tip.merchantBaseUrl,
|
||||
merchant_tip_id: tip.merchantRewardId,
|
||||
wallet_tip_id: tip.walletRewardId,
|
||||
next_url: tip.next_url,
|
||||
secret_seed: tip.secretSeed,
|
||||
selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({
|
||||
count: x.count,
|
||||
denom_pub_hash: x.denomPubHash,
|
||||
})),
|
||||
timestamp_finished: tip.pickedUpTimestamp,
|
||||
timestamp_accepted: tip.acceptedTimestamp,
|
||||
timestamp_created: tip.createdTimestamp,
|
||||
timestamp_expiration: tip.rewardExpiration,
|
||||
tip_amount_raw: Amounts.stringify(tip.rewardAmountRaw),
|
||||
selected_denoms_uid: tip.denomSelUid,
|
||||
});
|
||||
});
|
||||
|
||||
await tx.recoupGroups.iter().forEach((recoupGroup) => {
|
||||
backupRecoupGroups.push({
|
||||
recoup_group_id: recoupGroup.recoupGroupId,
|
||||
timestamp_created: recoupGroup.timestampStarted,
|
||||
timestamp_finish: recoupGroup.timestampFinished,
|
||||
coins: recoupGroup.coinPubs.map((x, i) => ({
|
||||
coin_pub: x,
|
||||
recoup_finished: recoupGroup.recoupFinishedPerCoin[i],
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
await tx.backupProviders.iter().forEach((bp) => {
|
||||
let terms: BackupBackupProviderTerms | undefined;
|
||||
if (bp.terms) {
|
||||
terms = {
|
||||
annual_fee: Amounts.stringify(bp.terms.annualFee),
|
||||
storage_limit_in_megabytes: bp.terms.storageLimitInMegabytes,
|
||||
supported_protocol_version: bp.terms.supportedProtocolVersion,
|
||||
};
|
||||
}
|
||||
backupBackupProviders.push({
|
||||
terms,
|
||||
base_url: canonicalizeBaseUrl(bp.baseUrl),
|
||||
pay_proposal_ids: bp.paymentProposalIds,
|
||||
uids: bp.uids,
|
||||
});
|
||||
});
|
||||
|
||||
await tx.coins.iter().forEach((coin) => {
|
||||
let bcs: BackupCoinSource;
|
||||
switch (coin.coinSource.type) {
|
||||
case CoinSourceType.Refresh:
|
||||
bcs = {
|
||||
type: BackupCoinSourceType.Refresh,
|
||||
old_coin_pub: coin.coinSource.oldCoinPub,
|
||||
refresh_group_id: coin.coinSource.refreshGroupId,
|
||||
};
|
||||
break;
|
||||
case CoinSourceType.Reward:
|
||||
bcs = {
|
||||
type: BackupCoinSourceType.Reward,
|
||||
coin_index: coin.coinSource.coinIndex,
|
||||
wallet_tip_id: coin.coinSource.walletRewardId,
|
||||
};
|
||||
break;
|
||||
case CoinSourceType.Withdraw:
|
||||
bcs = {
|
||||
type: BackupCoinSourceType.Withdraw,
|
||||
coin_index: coin.coinSource.coinIndex,
|
||||
reserve_pub: coin.coinSource.reservePub,
|
||||
withdrawal_group_id: coin.coinSource.withdrawalGroupId,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
const coins = (backupCoinsByDenom[coin.denomPubHash] ??= []);
|
||||
coins.push({
|
||||
blinding_key: coin.blindingKey,
|
||||
coin_priv: coin.coinPriv,
|
||||
coin_source: bcs,
|
||||
fresh: coin.status === CoinStatus.Fresh,
|
||||
spend_allocation: coin.spendAllocation
|
||||
? {
|
||||
amount: coin.spendAllocation.amount,
|
||||
id: coin.spendAllocation.id,
|
||||
}
|
||||
: undefined,
|
||||
denom_sig: coin.denomSig,
|
||||
});
|
||||
});
|
||||
|
||||
await tx.denominations.iter().forEach((denom) => {
|
||||
const backupDenoms = (backupDenominationsByExchange[
|
||||
denom.exchangeBaseUrl
|
||||
] ??= []);
|
||||
backupDenoms.push({
|
||||
coins: backupCoinsByDenom[denom.denomPubHash] ?? [],
|
||||
denom_pub: denom.denomPub,
|
||||
fee_deposit: Amounts.stringify(denom.fees.feeDeposit),
|
||||
fee_refresh: Amounts.stringify(denom.fees.feeRefresh),
|
||||
fee_refund: Amounts.stringify(denom.fees.feeRefund),
|
||||
fee_withdraw: Amounts.stringify(denom.fees.feeWithdraw),
|
||||
is_offered: denom.isOffered,
|
||||
is_revoked: denom.isRevoked,
|
||||
master_sig: denom.masterSig,
|
||||
stamp_expire_deposit: denom.stampExpireDeposit,
|
||||
stamp_expire_legal: denom.stampExpireLegal,
|
||||
stamp_expire_withdraw: denom.stampExpireWithdraw,
|
||||
stamp_start: denom.stampStart,
|
||||
value: Amounts.stringify(DenominationRecord.getValue(denom)),
|
||||
list_issue_date: denom.listIssueDate,
|
||||
});
|
||||
});
|
||||
|
||||
await tx.exchanges.iter().forEachAsync(async (ex) => {
|
||||
const dp = ex.detailsPointer;
|
||||
if (!dp) {
|
||||
return;
|
||||
}
|
||||
backupExchanges.push({
|
||||
base_url: ex.baseUrl,
|
||||
currency: dp.currency,
|
||||
master_public_key: dp.masterPublicKey,
|
||||
update_clock: dp.updateClock,
|
||||
});
|
||||
});
|
||||
|
||||
await tx.exchangeDetails.iter().forEachAsync(async (ex) => {
|
||||
// Only back up permanently added exchanges.
|
||||
|
||||
const wi = ex.wireInfo;
|
||||
const wireFees: BackupExchangeWireFee[] = [];
|
||||
|
||||
Object.keys(wi.feesForType).forEach((x) => {
|
||||
for (const f of wi.feesForType[x]) {
|
||||
wireFees.push({
|
||||
wire_type: x,
|
||||
closing_fee: Amounts.stringify(f.closingFee),
|
||||
end_stamp: f.endStamp,
|
||||
sig: f.sig,
|
||||
start_stamp: f.startStamp,
|
||||
wire_fee: Amounts.stringify(f.wireFee),
|
||||
});
|
||||
}
|
||||
});
|
||||
checkDbInvariant(ex.rowId != null);
|
||||
const exchangeSk =
|
||||
await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll(
|
||||
ex.rowId,
|
||||
);
|
||||
let signingKeys: BackupExchangeSignKey[] = exchangeSk.map((x) => ({
|
||||
key: x.signkeyPub,
|
||||
master_sig: x.masterSig,
|
||||
stamp_end: x.stampEnd,
|
||||
stamp_expire: x.stampExpire,
|
||||
stamp_start: x.stampStart,
|
||||
}));
|
||||
|
||||
backupExchangeDetails.push({
|
||||
base_url: ex.exchangeBaseUrl,
|
||||
reserve_closing_delay: ex.reserveClosingDelay,
|
||||
accounts: ex.wireInfo.accounts.map((x) => ({
|
||||
payto_uri: x.payto_uri,
|
||||
master_sig: x.master_sig,
|
||||
})),
|
||||
auditors: ex.auditors.map((x) => ({
|
||||
auditor_pub: x.auditor_pub,
|
||||
auditor_url: x.auditor_url,
|
||||
denomination_keys: x.denomination_keys,
|
||||
})),
|
||||
master_public_key: ex.masterPublicKey,
|
||||
currency: ex.currency,
|
||||
protocol_version: ex.protocolVersionRange,
|
||||
wire_fees: wireFees,
|
||||
signing_keys: signingKeys,
|
||||
global_fees: ex.globalFees.map((x) => ({
|
||||
accountFee: Amounts.stringify(x.accountFee),
|
||||
historyFee: Amounts.stringify(x.historyFee),
|
||||
purseFee: Amounts.stringify(x.purseFee),
|
||||
endDate: x.endDate,
|
||||
historyTimeout: x.historyTimeout,
|
||||
signature: x.signature,
|
||||
purseLimit: x.purseLimit,
|
||||
purseTimeout: x.purseTimeout,
|
||||
startDate: x.startDate,
|
||||
})),
|
||||
tos_accepted_etag: ex.tosAccepted?.etag,
|
||||
tos_accepted_timestamp: ex.tosAccepted?.timestamp,
|
||||
denominations:
|
||||
backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
|
||||
});
|
||||
});
|
||||
|
||||
const purchaseProposalIdSet = new Set<string>();
|
||||
|
||||
await tx.purchases.iter().forEachAsync(async (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;
|
||||
// }
|
||||
// }
|
||||
|
||||
let propStatus: BackupProposalStatus;
|
||||
switch (purch.purchaseStatus) {
|
||||
case PurchaseStatus.Done:
|
||||
case PurchaseStatus.PendingQueryingAutoRefund:
|
||||
case PurchaseStatus.PendingQueryingRefund:
|
||||
propStatus = BackupProposalStatus.Paid;
|
||||
break;
|
||||
case PurchaseStatus.PendingPayingReplay:
|
||||
case PurchaseStatus.PendingDownloadingProposal:
|
||||
case PurchaseStatus.DialogProposed:
|
||||
case PurchaseStatus.PendingPaying:
|
||||
propStatus = BackupProposalStatus.Proposed;
|
||||
break;
|
||||
case PurchaseStatus.DialogShared:
|
||||
propStatus = BackupProposalStatus.Shared;
|
||||
break;
|
||||
case PurchaseStatus.FailedClaim:
|
||||
case PurchaseStatus.AbortedIncompletePayment:
|
||||
propStatus = BackupProposalStatus.PermanentlyFailed;
|
||||
break;
|
||||
case PurchaseStatus.AbortingWithRefund:
|
||||
case PurchaseStatus.AbortedProposalRefused:
|
||||
propStatus = BackupProposalStatus.Refused;
|
||||
break;
|
||||
case PurchaseStatus.RepurchaseDetected:
|
||||
propStatus = BackupProposalStatus.Repurchase;
|
||||
break;
|
||||
default: {
|
||||
const error = purch.purchaseStatus;
|
||||
throw Error(`purchase status ${error} is not handled`);
|
||||
}
|
||||
}
|
||||
|
||||
const payInfo = purch.payInfo;
|
||||
let backupPayInfo: BackupPayInfo | undefined = undefined;
|
||||
if (payInfo) {
|
||||
backupPayInfo = {
|
||||
pay_coins: payInfo.payCoinSelection.coinPubs.map((x, i) => ({
|
||||
coin_pub: x,
|
||||
contribution: Amounts.stringify(
|
||||
payInfo.payCoinSelection.coinContributions[i],
|
||||
),
|
||||
})),
|
||||
total_pay_cost: Amounts.stringify(payInfo.totalPayCost),
|
||||
pay_coins_uid: payInfo.payCoinSelectionUid,
|
||||
};
|
||||
}
|
||||
|
||||
let contractTermsRaw = undefined;
|
||||
if (purch.download) {
|
||||
const contractTermsRecord = await tx.contractTerms.get(
|
||||
purch.download.contractTermsHash,
|
||||
);
|
||||
if (contractTermsRecord) {
|
||||
contractTermsRaw = contractTermsRecord.contractTermsRaw;
|
||||
}
|
||||
}
|
||||
|
||||
backupPurchases.push({
|
||||
contract_terms_raw: contractTermsRaw,
|
||||
auto_refund_deadline: purch.autoRefundDeadline,
|
||||
merchant_pay_sig: purch.merchantPaySig,
|
||||
pos_confirmation: purch.posConfirmation,
|
||||
pay_info: backupPayInfo,
|
||||
proposal_id: purch.proposalId,
|
||||
refunds,
|
||||
timestamp_accepted: purch.timestampAccept,
|
||||
timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay,
|
||||
nonce_priv: purch.noncePriv,
|
||||
merchant_sig: purch.download?.contractTermsMerchantSig,
|
||||
claim_token: purch.claimToken,
|
||||
merchant_base_url: purch.merchantBaseUrl,
|
||||
order_id: purch.orderId,
|
||||
proposal_status: propStatus,
|
||||
repurchase_proposal_id: purch.repurchaseProposalId,
|
||||
download_session_id: purch.downloadSessionId,
|
||||
timestamp_proposed: purch.timestamp,
|
||||
shared: purch.shared,
|
||||
});
|
||||
});
|
||||
|
||||
await tx.refreshGroups.iter().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.statusPerCoin[i] === RefreshCoinStatus.Finished,
|
||||
input_amount: Amounts.stringify(rg.inputPerCoin[i]),
|
||||
refresh_session: refreshSession,
|
||||
});
|
||||
}
|
||||
|
||||
backupRefreshGroups.push({
|
||||
reason: rg.reason as any,
|
||||
refresh_group_id: rg.refreshGroupId,
|
||||
timestamp_created: rg.timestampCreated,
|
||||
timestamp_finish: rg.timestampFinished,
|
||||
old_coins: oldCoins,
|
||||
});
|
||||
});
|
||||
|
||||
const ts = TalerPreciseTimestamp.now();
|
||||
|
||||
if (!bs.lastBackupTimestamp) {
|
||||
bs.lastBackupTimestamp = ts;
|
||||
}
|
||||
|
||||
const backupBlob: WalletBackupContentV1 = {
|
||||
schema_id: "gnu-taler-wallet-backup-content",
|
||||
schema_version: BACKUP_VERSION_MAJOR,
|
||||
minor_version: BACKUP_VERSION_MINOR,
|
||||
exchanges: backupExchanges,
|
||||
exchange_details: backupExchangeDetails,
|
||||
wallet_root_pub: bs.walletRootPub,
|
||||
backup_providers: backupBackupProviders,
|
||||
current_device_id: bs.deviceId,
|
||||
purchases: backupPurchases,
|
||||
recoup_groups: backupRecoupGroups,
|
||||
refresh_groups: backupRefreshGroups,
|
||||
tips: backupTips,
|
||||
timestamp: bs.lastBackupTimestamp,
|
||||
trusted_auditors: {},
|
||||
trusted_exchanges: {},
|
||||
intern_table: {},
|
||||
error_reports: [],
|
||||
tombstones: [],
|
||||
// FIXME!
|
||||
withdrawal_groups: backupWithdrawalGroups,
|
||||
};
|
||||
|
||||
// If the backup changed, we change our nonce and timestamp.
|
||||
|
||||
let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob))));
|
||||
if (h !== bs.lastBackupPlainHash) {
|
||||
logger.trace(
|
||||
`plain backup hash changed (from ${bs.lastBackupPlainHash}to ${h})`,
|
||||
);
|
||||
bs.lastBackupTimestamp = ts;
|
||||
backupBlob.timestamp = ts;
|
||||
bs.lastBackupPlainHash = encodeCrock(
|
||||
hash(stringToBytes(canonicalJson(backupBlob))),
|
||||
);
|
||||
bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
|
||||
logger.trace(
|
||||
`setting timestamp to ${AbsoluteTime.toIsoString(
|
||||
AbsoluteTime.fromPreciseTimestamp(ts),
|
||||
)} and nonce to ${bs.lastBackupNonce}`,
|
||||
);
|
||||
await tx.config.put({
|
||||
key: ConfigRecordKey.WalletBackupState,
|
||||
value: bs,
|
||||
});
|
||||
} else {
|
||||
logger.trace("backup hash did not change");
|
||||
}
|
||||
|
||||
return backupBlob;
|
||||
});
|
||||
}
|
@ -1,874 +0,0 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2020 Taler Systems SA
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import {
|
||||
AgeRestriction,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
BackupCoin,
|
||||
BackupCoinSourceType,
|
||||
BackupDenomSel,
|
||||
BackupPayInfo,
|
||||
BackupProposalStatus,
|
||||
BackupRefreshReason,
|
||||
BackupRefundState,
|
||||
BackupWgType,
|
||||
codecForMerchantContractTerms,
|
||||
CoinStatus,
|
||||
DenomKeyType,
|
||||
DenomSelectionState,
|
||||
j2s,
|
||||
Logger,
|
||||
PayCoinSelection,
|
||||
RefreshReason,
|
||||
TalerProtocolTimestamp,
|
||||
TalerPreciseTimestamp,
|
||||
WalletBackupContentV1,
|
||||
WireInfo,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
CoinRecord,
|
||||
CoinSource,
|
||||
CoinSourceType,
|
||||
DenominationRecord,
|
||||
DenominationVerificationStatus,
|
||||
ProposalDownloadInfo,
|
||||
PurchaseStatus,
|
||||
PurchasePayInfo,
|
||||
RefreshCoinStatus,
|
||||
RefreshSessionRecord,
|
||||
WalletContractData,
|
||||
WalletStoresV1,
|
||||
WgInfo,
|
||||
WithdrawalGroupStatus,
|
||||
WithdrawalRecordType,
|
||||
RefreshOperationStatus,
|
||||
RewardRecordStatus,
|
||||
} from "../../db.js";
|
||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
||||
import { checkLogicInvariant } from "../../util/invariants.js";
|
||||
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
||||
import {
|
||||
constructTombstone,
|
||||
makeCoinAvailable,
|
||||
TombstoneTag,
|
||||
} from "../common.js";
|
||||
import { getExchangeDetails } from "../exchanges.js";
|
||||
import { extractContractData } from "../pay-merchant.js";
|
||||
import { provideBackupState } from "./state.js";
|
||||
|
||||
const logger = new Logger("operations/backup/import.ts");
|
||||
|
||||
function checkBackupInvariant(b: boolean, m?: string): asserts b {
|
||||
if (!b) {
|
||||
if (m) {
|
||||
throw Error(`BUG: backup invariant failed (${m})`);
|
||||
} else {
|
||||
throw Error("BUG: backup invariant failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-compute information about the coin selection for a payment.
|
||||
*/
|
||||
async function recoverPayCoinSelection(
|
||||
tx: GetReadWriteAccess<{
|
||||
exchanges: typeof WalletStoresV1.exchanges;
|
||||
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
||||
coins: typeof WalletStoresV1.coins;
|
||||
denominations: typeof WalletStoresV1.denominations;
|
||||
}>,
|
||||
contractData: WalletContractData,
|
||||
payInfo: BackupPayInfo,
|
||||
): Promise<PayCoinSelection> {
|
||||
const coinPubs: string[] = payInfo.pay_coins.map((x) => x.coin_pub);
|
||||
const coinContributions: AmountJson[] = payInfo.pay_coins.map((x) =>
|
||||
Amounts.parseOrThrow(x.contribution),
|
||||
);
|
||||
|
||||
const coveredExchanges: Set<string> = new Set();
|
||||
|
||||
let totalWireFee: AmountJson = Amounts.zeroOfAmount(contractData.amount);
|
||||
let totalDepositFees: AmountJson = Amounts.zeroOfAmount(contractData.amount);
|
||||
|
||||
for (const coinPub of coinPubs) {
|
||||
const coinRecord = await tx.coins.get(coinPub);
|
||||
checkBackupInvariant(!!coinRecord);
|
||||
const denom = await tx.denominations.get([
|
||||
coinRecord.exchangeBaseUrl,
|
||||
coinRecord.denomPubHash,
|
||||
]);
|
||||
checkBackupInvariant(!!denom);
|
||||
totalDepositFees = Amounts.add(
|
||||
totalDepositFees,
|
||||
denom.fees.feeDeposit,
|
||||
).amount;
|
||||
|
||||
if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
|
||||
const exchangeDetails = await getExchangeDetails(
|
||||
tx,
|
||||
coinRecord.exchangeBaseUrl,
|
||||
);
|
||||
checkBackupInvariant(!!exchangeDetails);
|
||||
let wireFee: AmountJson | undefined;
|
||||
const feesForType = exchangeDetails.wireInfo.feesForType;
|
||||
checkBackupInvariant(!!feesForType);
|
||||
for (const fee of feesForType[contractData.wireMethod] || []) {
|
||||
if (
|
||||
fee.startStamp <= contractData.timestamp &&
|
||||
fee.endStamp >= contractData.timestamp
|
||||
) {
|
||||
wireFee = Amounts.parseOrThrow(fee.wireFee);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (wireFee) {
|
||||
totalWireFee = Amounts.add(totalWireFee, wireFee).amount;
|
||||
}
|
||||
coveredExchanges.add(coinRecord.exchangeBaseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
let customerWireFee: AmountJson;
|
||||
|
||||
const amortizedWireFee = Amounts.divide(
|
||||
totalWireFee,
|
||||
contractData.wireFeeAmortization,
|
||||
);
|
||||
if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
|
||||
customerWireFee = amortizedWireFee;
|
||||
} else {
|
||||
customerWireFee = Amounts.zeroOfAmount(contractData.amount);
|
||||
}
|
||||
|
||||
const customerDepositFees = Amounts.sub(
|
||||
totalDepositFees,
|
||||
contractData.maxDepositFee,
|
||||
).amount;
|
||||
|
||||
return {
|
||||
coinPubs,
|
||||
coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
|
||||
paymentAmount: Amounts.stringify(contractData.amount),
|
||||
customerWireFees: Amounts.stringify(customerWireFee),
|
||||
customerDepositFees: Amounts.stringify(customerDepositFees),
|
||||
};
|
||||
}
|
||||
|
||||
async function getDenomSelStateFromBackup(
|
||||
tx: GetReadOnlyAccess<{ denominations: typeof WalletStoresV1.denominations }>,
|
||||
currency: string,
|
||||
exchangeBaseUrl: string,
|
||||
sel: BackupDenomSel,
|
||||
): Promise<DenomSelectionState> {
|
||||
const selectedDenoms: {
|
||||
denomPubHash: string;
|
||||
count: number;
|
||||
}[] = [];
|
||||
let totalCoinValue = Amounts.zeroOfCurrency(currency);
|
||||
let totalWithdrawCost = Amounts.zeroOfCurrency(currency);
|
||||
for (const s of sel) {
|
||||
const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]);
|
||||
checkBackupInvariant(!!d);
|
||||
totalCoinValue = Amounts.add(
|
||||
totalCoinValue,
|
||||
DenominationRecord.getValue(d),
|
||||
).amount;
|
||||
totalWithdrawCost = Amounts.add(
|
||||
totalWithdrawCost,
|
||||
DenominationRecord.getValue(d),
|
||||
d.fees.feeWithdraw,
|
||||
).amount;
|
||||
}
|
||||
return {
|
||||
selectedDenoms,
|
||||
totalCoinValue: Amounts.stringify(totalCoinValue),
|
||||
totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompletedCoin {
|
||||
coinPub: string;
|
||||
coinEvHash: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Precomputed cryptographic material for a backup import.
|
||||
*
|
||||
* We separate this data from the backup blob as we want the backup
|
||||
* blob to be small, and we can't compute it during the database transaction,
|
||||
* as the async crypto worker communication would auto-close the database transaction.
|
||||
*/
|
||||
export interface BackupCryptoPrecomputedData {
|
||||
rsaDenomPubToHash: Record<string, string>;
|
||||
coinPrivToCompletedCoin: Record<string, CompletedCoin>;
|
||||
proposalNoncePrivToPub: { [priv: string]: string };
|
||||
proposalIdToContractTermsHash: { [proposalId: string]: string };
|
||||
reservePrivToPub: Record<string, string>;
|
||||
}
|
||||
|
||||
export async function importCoin(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadWriteAccess<{
|
||||
coins: typeof WalletStoresV1.coins;
|
||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||
denominations: typeof WalletStoresV1.denominations;
|
||||
}>,
|
||||
cryptoComp: BackupCryptoPrecomputedData,
|
||||
args: {
|
||||
backupCoin: BackupCoin;
|
||||
exchangeBaseUrl: string;
|
||||
denomPubHash: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const { backupCoin, exchangeBaseUrl, denomPubHash } = args;
|
||||
const compCoin = cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
|
||||
checkLogicInvariant(!!compCoin);
|
||||
const existingCoin = await tx.coins.get(compCoin.coinPub);
|
||||
if (!existingCoin) {
|
||||
let coinSource: CoinSource;
|
||||
switch (backupCoin.coin_source.type) {
|
||||
case BackupCoinSourceType.Refresh:
|
||||
coinSource = {
|
||||
type: CoinSourceType.Refresh,
|
||||
oldCoinPub: backupCoin.coin_source.old_coin_pub,
|
||||
refreshGroupId: backupCoin.coin_source.refresh_group_id,
|
||||
};
|
||||
break;
|
||||
case BackupCoinSourceType.Reward:
|
||||
coinSource = {
|
||||
type: CoinSourceType.Reward,
|
||||
coinIndex: backupCoin.coin_source.coin_index,
|
||||
walletRewardId: backupCoin.coin_source.wallet_tip_id,
|
||||
};
|
||||
break;
|
||||
case BackupCoinSourceType.Withdraw:
|
||||
coinSource = {
|
||||
type: CoinSourceType.Withdraw,
|
||||
coinIndex: backupCoin.coin_source.coin_index,
|
||||
reservePub: backupCoin.coin_source.reserve_pub,
|
||||
withdrawalGroupId: backupCoin.coin_source.withdrawal_group_id,
|
||||
};
|
||||
break;
|
||||
}
|
||||
const coinRecord: CoinRecord = {
|
||||
blindingKey: backupCoin.blinding_key,
|
||||
coinEvHash: compCoin.coinEvHash,
|
||||
coinPriv: backupCoin.coin_priv,
|
||||
denomSig: backupCoin.denom_sig,
|
||||
coinPub: compCoin.coinPub,
|
||||
exchangeBaseUrl,
|
||||
denomPubHash,
|
||||
status: backupCoin.fresh ? CoinStatus.Fresh : CoinStatus.Dormant,
|
||||
coinSource,
|
||||
// FIXME!
|
||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
||||
// FIXME!
|
||||
ageCommitmentProof: undefined,
|
||||
// FIXME!
|
||||
spendAllocation: undefined,
|
||||
};
|
||||
if (coinRecord.status === CoinStatus.Fresh) {
|
||||
await makeCoinAvailable(ws, tx, coinRecord);
|
||||
} else {
|
||||
await tx.coins.put(coinRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function importBackup(
|
||||
ws: InternalWalletState,
|
||||
backupBlobArg: any,
|
||||
cryptoComp: BackupCryptoPrecomputedData,
|
||||
): Promise<void> {
|
||||
await provideBackupState(ws);
|
||||
|
||||
logger.info(`importing backup ${j2s(backupBlobArg)}`);
|
||||
|
||||
return ws.db
|
||||
.mktx((x) => [
|
||||
x.config,
|
||||
x.exchangeDetails,
|
||||
x.exchanges,
|
||||
x.coins,
|
||||
x.coinAvailability,
|
||||
x.denominations,
|
||||
x.purchases,
|
||||
x.refreshGroups,
|
||||
x.backupProviders,
|
||||
x.rewards,
|
||||
x.recoupGroups,
|
||||
x.withdrawalGroups,
|
||||
x.tombstones,
|
||||
x.depositGroups,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
// FIXME: validate schema!
|
||||
const backupBlob = backupBlobArg as WalletBackupContentV1;
|
||||
|
||||
// FIXME: validate version
|
||||
|
||||
for (const tombstone of backupBlob.tombstones) {
|
||||
await tx.tombstones.put({
|
||||
id: tombstone,
|
||||
});
|
||||
}
|
||||
|
||||
const tombstoneSet = new Set(
|
||||
(await tx.tombstones.iter().toArray()).map((x) => x.id),
|
||||
);
|
||||
|
||||
// FIXME: Validate that the "details pointer" is correct
|
||||
|
||||
for (const backupExchange of backupBlob.exchanges) {
|
||||
const existingExchange = await tx.exchanges.get(
|
||||
backupExchange.base_url,
|
||||
);
|
||||
if (existingExchange) {
|
||||
continue;
|
||||
}
|
||||
// await tx.exchanges.put({
|
||||
// baseUrl: backupExchange.base_url,
|
||||
// detailsPointer: {
|
||||
// currency: backupExchange.currency,
|
||||
// masterPublicKey: backupExchange.master_public_key,
|
||||
// updateClock: backupExchange.update_clock,
|
||||
// },
|
||||
// lastUpdate: undefined,
|
||||
// nextUpdate: TalerPreciseTimestamp.now(),
|
||||
// nextRefreshCheck: TalerPreciseTimestamp.now(),
|
||||
// lastKeysEtag: undefined,
|
||||
// lastWireEtag: undefined,
|
||||
// });
|
||||
}
|
||||
|
||||
for (const backupExchangeDetails of backupBlob.exchange_details) {
|
||||
const existingExchangeDetails =
|
||||
await tx.exchangeDetails.indexes.byPointer.get([
|
||||
backupExchangeDetails.base_url,
|
||||
backupExchangeDetails.currency,
|
||||
backupExchangeDetails.master_public_key,
|
||||
]);
|
||||
|
||||
if (!existingExchangeDetails) {
|
||||
const wireInfo: WireInfo = {
|
||||
accounts: backupExchangeDetails.accounts.map((x) => ({
|
||||
master_sig: x.master_sig,
|
||||
payto_uri: x.payto_uri,
|
||||
})),
|
||||
feesForType: {},
|
||||
};
|
||||
for (const fee of backupExchangeDetails.wire_fees) {
|
||||
const w = (wireInfo.feesForType[fee.wire_type] ??= []);
|
||||
w.push({
|
||||
closingFee: Amounts.stringify(fee.closing_fee),
|
||||
endStamp: fee.end_stamp,
|
||||
sig: fee.sig,
|
||||
startStamp: fee.start_stamp,
|
||||
wireFee: Amounts.stringify(fee.wire_fee),
|
||||
});
|
||||
}
|
||||
let tosAccepted = undefined;
|
||||
if (
|
||||
backupExchangeDetails.tos_accepted_etag &&
|
||||
backupExchangeDetails.tos_accepted_timestamp
|
||||
) {
|
||||
tosAccepted = {
|
||||
etag: backupExchangeDetails.tos_accepted_etag,
|
||||
timestamp: backupExchangeDetails.tos_accepted_timestamp,
|
||||
};
|
||||
}
|
||||
await tx.exchangeDetails.put({
|
||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||
wireInfo,
|
||||
currency: backupExchangeDetails.currency,
|
||||
auditors: backupExchangeDetails.auditors.map((x) => ({
|
||||
auditor_pub: x.auditor_pub,
|
||||
auditor_url: x.auditor_url,
|
||||
denomination_keys: x.denomination_keys,
|
||||
})),
|
||||
masterPublicKey: backupExchangeDetails.master_public_key,
|
||||
protocolVersionRange: backupExchangeDetails.protocol_version,
|
||||
reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
|
||||
tosCurrentEtag: backupExchangeDetails.tos_accepted_etag || "",
|
||||
tosAccepted,
|
||||
globalFees: backupExchangeDetails.global_fees.map((x) => ({
|
||||
accountFee: Amounts.stringify(x.accountFee),
|
||||
historyFee: Amounts.stringify(x.historyFee),
|
||||
purseFee: Amounts.stringify(x.purseFee),
|
||||
endDate: x.endDate,
|
||||
historyTimeout: x.historyTimeout,
|
||||
signature: x.signature,
|
||||
purseLimit: x.purseLimit,
|
||||
purseTimeout: x.purseTimeout,
|
||||
startDate: x.startDate,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
for (const backupDenomination of backupExchangeDetails.denominations) {
|
||||
if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) {
|
||||
throw Error("unsupported cipher");
|
||||
}
|
||||
const denomPubHash =
|
||||
cryptoComp.rsaDenomPubToHash[
|
||||
backupDenomination.denom_pub.rsa_public_key
|
||||
];
|
||||
checkLogicInvariant(!!denomPubHash);
|
||||
const existingDenom = await tx.denominations.get([
|
||||
backupExchangeDetails.base_url,
|
||||
denomPubHash,
|
||||
]);
|
||||
if (!existingDenom) {
|
||||
const value = Amounts.parseOrThrow(backupDenomination.value);
|
||||
|
||||
await tx.denominations.put({
|
||||
denomPub: backupDenomination.denom_pub,
|
||||
denomPubHash: denomPubHash,
|
||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||
exchangeMasterPub: backupExchangeDetails.master_public_key,
|
||||
fees: {
|
||||
feeDeposit: Amounts.stringify(backupDenomination.fee_deposit),
|
||||
feeRefresh: Amounts.stringify(backupDenomination.fee_refresh),
|
||||
feeRefund: Amounts.stringify(backupDenomination.fee_refund),
|
||||
feeWithdraw: Amounts.stringify(backupDenomination.fee_withdraw),
|
||||
},
|
||||
isOffered: backupDenomination.is_offered,
|
||||
isRevoked: backupDenomination.is_revoked,
|
||||
masterSig: backupDenomination.master_sig,
|
||||
stampExpireDeposit: backupDenomination.stamp_expire_deposit,
|
||||
stampExpireLegal: backupDenomination.stamp_expire_legal,
|
||||
stampExpireWithdraw: backupDenomination.stamp_expire_withdraw,
|
||||
stampStart: backupDenomination.stamp_start,
|
||||
verificationStatus: DenominationVerificationStatus.VerifiedGood,
|
||||
currency: value.currency,
|
||||
amountFrac: value.fraction,
|
||||
amountVal: value.value,
|
||||
listIssueDate: backupDenomination.list_issue_date,
|
||||
});
|
||||
}
|
||||
for (const backupCoin of backupDenomination.coins) {
|
||||
await importCoin(ws, tx, cryptoComp, {
|
||||
backupCoin,
|
||||
denomPubHash,
|
||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const backupWg of backupBlob.withdrawal_groups) {
|
||||
const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv];
|
||||
checkLogicInvariant(!!reservePub);
|
||||
const ts = constructTombstone({
|
||||
tag: TombstoneTag.DeleteReserve,
|
||||
reservePub,
|
||||
});
|
||||
if (tombstoneSet.has(ts)) {
|
||||
continue;
|
||||
}
|
||||
const existingWg = await tx.withdrawalGroups.get(
|
||||
backupWg.withdrawal_group_id,
|
||||
);
|
||||
if (existingWg) {
|
||||
continue;
|
||||
}
|
||||
let wgInfo: WgInfo;
|
||||
switch (backupWg.info.type) {
|
||||
case BackupWgType.BankIntegrated:
|
||||
wgInfo = {
|
||||
withdrawalType: WithdrawalRecordType.BankIntegrated,
|
||||
bankInfo: {
|
||||
exchangePaytoUri: backupWg.info.exchange_payto_uri,
|
||||
talerWithdrawUri: backupWg.info.taler_withdraw_uri,
|
||||
confirmUrl: backupWg.info.confirm_url,
|
||||
timestampBankConfirmed: backupWg.info.timestamp_bank_confirmed,
|
||||
timestampReserveInfoPosted:
|
||||
backupWg.info.timestamp_reserve_info_posted,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case BackupWgType.BankManual:
|
||||
wgInfo = {
|
||||
withdrawalType: WithdrawalRecordType.BankManual,
|
||||
};
|
||||
break;
|
||||
case BackupWgType.PeerPullCredit:
|
||||
wgInfo = {
|
||||
withdrawalType: WithdrawalRecordType.PeerPullCredit,
|
||||
contractTerms: backupWg.info.contract_terms,
|
||||
contractPriv: backupWg.info.contract_priv,
|
||||
};
|
||||
break;
|
||||
case BackupWgType.PeerPushCredit:
|
||||
wgInfo = {
|
||||
withdrawalType: WithdrawalRecordType.PeerPushCredit,
|
||||
contractTerms: backupWg.info.contract_terms,
|
||||
};
|
||||
break;
|
||||
case BackupWgType.Recoup:
|
||||
wgInfo = {
|
||||
withdrawalType: WithdrawalRecordType.Recoup,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(backupWg.info);
|
||||
}
|
||||
const instructedAmount = Amounts.parseOrThrow(
|
||||
backupWg.instructed_amount,
|
||||
);
|
||||
await tx.withdrawalGroups.put({
|
||||
withdrawalGroupId: backupWg.withdrawal_group_id,
|
||||
exchangeBaseUrl: backupWg.exchange_base_url,
|
||||
instructedAmount: Amounts.stringify(instructedAmount),
|
||||
secretSeed: backupWg.secret_seed,
|
||||
denomsSel: await getDenomSelStateFromBackup(
|
||||
tx,
|
||||
instructedAmount.currency,
|
||||
backupWg.exchange_base_url,
|
||||
backupWg.selected_denoms,
|
||||
),
|
||||
denomSelUid: backupWg.selected_denoms_uid,
|
||||
rawWithdrawalAmount: Amounts.stringify(
|
||||
backupWg.raw_withdrawal_amount,
|
||||
),
|
||||
effectiveWithdrawalAmount: Amounts.stringify(
|
||||
backupWg.effective_withdrawal_amount,
|
||||
),
|
||||
reservePriv: backupWg.reserve_priv,
|
||||
reservePub,
|
||||
status: backupWg.timestamp_finish
|
||||
? WithdrawalGroupStatus.Finished
|
||||
: WithdrawalGroupStatus.PendingQueryingStatus, // FIXME!
|
||||
timestampStart: backupWg.timestamp_created,
|
||||
wgInfo,
|
||||
restrictAge: backupWg.restrict_age,
|
||||
senderWire: undefined, // FIXME!
|
||||
timestampFinish: backupWg.timestamp_finish,
|
||||
});
|
||||
}
|
||||
|
||||
for (const backupPurchase of backupBlob.purchases) {
|
||||
const ts = constructTombstone({
|
||||
tag: TombstoneTag.DeletePayment,
|
||||
proposalId: backupPurchase.proposal_id,
|
||||
});
|
||||
if (tombstoneSet.has(ts)) {
|
||||
continue;
|
||||
}
|
||||
const existingPurchase = await tx.purchases.get(
|
||||
backupPurchase.proposal_id,
|
||||
);
|
||||
let proposalStatus: PurchaseStatus;
|
||||
switch (backupPurchase.proposal_status) {
|
||||
case BackupProposalStatus.Paid:
|
||||
proposalStatus = PurchaseStatus.Done;
|
||||
break;
|
||||
case BackupProposalStatus.Shared:
|
||||
proposalStatus = PurchaseStatus.DialogShared;
|
||||
break;
|
||||
case BackupProposalStatus.Proposed:
|
||||
proposalStatus = PurchaseStatus.DialogProposed;
|
||||
break;
|
||||
case BackupProposalStatus.PermanentlyFailed:
|
||||
proposalStatus = PurchaseStatus.AbortedIncompletePayment;
|
||||
break;
|
||||
case BackupProposalStatus.Refused:
|
||||
proposalStatus = PurchaseStatus.AbortedProposalRefused;
|
||||
break;
|
||||
case BackupProposalStatus.Repurchase:
|
||||
proposalStatus = PurchaseStatus.RepurchaseDetected;
|
||||
break;
|
||||
default: {
|
||||
const error: never = backupPurchase.proposal_status;
|
||||
throw Error(`backup status ${error} is not handled`);
|
||||
}
|
||||
}
|
||||
if (!existingPurchase) {
|
||||
//const refunds: { [refundKey: string]: WalletRefundItem } = {};
|
||||
// for (const backupRefund of backupPurchase.refunds) {
|
||||
// const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
|
||||
// const coin = await tx.coins.get(backupRefund.coin_pub);
|
||||
// checkBackupInvariant(!!coin);
|
||||
// const denom = await tx.denominations.get([
|
||||
// coin.exchangeBaseUrl,
|
||||
// coin.denomPubHash,
|
||||
// ]);
|
||||
// checkBackupInvariant(!!denom);
|
||||
// const common = {
|
||||
// coinPub: backupRefund.coin_pub,
|
||||
// executionTime: backupRefund.execution_time,
|
||||
// obtainedTime: backupRefund.obtained_time,
|
||||
// refundAmount: Amounts.stringify(backupRefund.refund_amount),
|
||||
// refundFee: Amounts.stringify(denom.fees.feeRefund),
|
||||
// rtransactionId: backupRefund.rtransaction_id,
|
||||
// totalRefreshCostBound: Amounts.stringify(
|
||||
// backupRefund.total_refresh_cost_bound,
|
||||
// ),
|
||||
// };
|
||||
// switch (backupRefund.type) {
|
||||
// case BackupRefundState.Applied:
|
||||
// refunds[key] = {
|
||||
// type: RefundState.Applied,
|
||||
// ...common,
|
||||
// };
|
||||
// break;
|
||||
// case BackupRefundState.Failed:
|
||||
// refunds[key] = {
|
||||
// type: RefundState.Failed,
|
||||
// ...common,
|
||||
// };
|
||||
// break;
|
||||
// case BackupRefundState.Pending:
|
||||
// refunds[key] = {
|
||||
// type: RefundState.Pending,
|
||||
// ...common,
|
||||
// };
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
const parsedContractTerms = codecForMerchantContractTerms().decode(
|
||||
backupPurchase.contract_terms_raw,
|
||||
);
|
||||
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
||||
const contractTermsHash =
|
||||
cryptoComp.proposalIdToContractTermsHash[
|
||||
backupPurchase.proposal_id
|
||||
];
|
||||
let maxWireFee: AmountJson;
|
||||
if (parsedContractTerms.max_wire_fee) {
|
||||
maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
|
||||
} else {
|
||||
maxWireFee = Amounts.zeroOfCurrency(amount.currency);
|
||||
}
|
||||
const download: ProposalDownloadInfo = {
|
||||
contractTermsHash,
|
||||
contractTermsMerchantSig: backupPurchase.merchant_sig!,
|
||||
currency: amount.currency,
|
||||
fulfillmentUrl: backupPurchase.contract_terms_raw.fulfillment_url,
|
||||
};
|
||||
|
||||
const contractData = extractContractData(
|
||||
backupPurchase.contract_terms_raw,
|
||||
contractTermsHash,
|
||||
download.contractTermsMerchantSig,
|
||||
);
|
||||
|
||||
let payInfo: PurchasePayInfo | undefined = undefined;
|
||||
if (backupPurchase.pay_info) {
|
||||
payInfo = {
|
||||
payCoinSelection: await recoverPayCoinSelection(
|
||||
tx,
|
||||
contractData,
|
||||
backupPurchase.pay_info,
|
||||
),
|
||||
payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid,
|
||||
totalPayCost: Amounts.stringify(
|
||||
backupPurchase.pay_info.total_pay_cost,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
await tx.purchases.put({
|
||||
proposalId: backupPurchase.proposal_id,
|
||||
noncePriv: backupPurchase.nonce_priv,
|
||||
noncePub:
|
||||
cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
|
||||
autoRefundDeadline: TalerProtocolTimestamp.never(),
|
||||
timestampAccept: backupPurchase.timestamp_accepted,
|
||||
timestampFirstSuccessfulPay:
|
||||
backupPurchase.timestamp_first_successful_pay,
|
||||
timestampLastRefundStatus: undefined,
|
||||
merchantPaySig: backupPurchase.merchant_pay_sig,
|
||||
posConfirmation: backupPurchase.pos_confirmation,
|
||||
lastSessionId: undefined,
|
||||
download,
|
||||
//refunds,
|
||||
claimToken: backupPurchase.claim_token,
|
||||
downloadSessionId: backupPurchase.download_session_id,
|
||||
merchantBaseUrl: backupPurchase.merchant_base_url,
|
||||
orderId: backupPurchase.order_id,
|
||||
payInfo,
|
||||
refundAmountAwaiting: undefined,
|
||||
repurchaseProposalId: backupPurchase.repurchase_proposal_id,
|
||||
purchaseStatus: proposalStatus,
|
||||
timestamp: backupPurchase.timestamp_proposed,
|
||||
shared: backupPurchase.shared,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const backupRefreshGroup of backupBlob.refresh_groups) {
|
||||
const ts = constructTombstone({
|
||||
tag: TombstoneTag.DeleteRefreshGroup,
|
||||
refreshGroupId: backupRefreshGroup.refresh_group_id,
|
||||
});
|
||||
if (tombstoneSet.has(ts)) {
|
||||
continue;
|
||||
}
|
||||
const existingRg = await tx.refreshGroups.get(
|
||||
backupRefreshGroup.refresh_group_id,
|
||||
);
|
||||
if (!existingRg) {
|
||||
let reason: RefreshReason;
|
||||
switch (backupRefreshGroup.reason) {
|
||||
case BackupRefreshReason.AbortPay:
|
||||
reason = RefreshReason.AbortPay;
|
||||
break;
|
||||
case BackupRefreshReason.BackupRestored:
|
||||
reason = RefreshReason.BackupRestored;
|
||||
break;
|
||||
case BackupRefreshReason.Manual:
|
||||
reason = RefreshReason.Manual;
|
||||
break;
|
||||
case BackupRefreshReason.Pay:
|
||||
reason = RefreshReason.PayMerchant;
|
||||
break;
|
||||
case BackupRefreshReason.Recoup:
|
||||
reason = RefreshReason.Recoup;
|
||||
break;
|
||||
case BackupRefreshReason.Refund:
|
||||
reason = RefreshReason.Refund;
|
||||
break;
|
||||
case BackupRefreshReason.Scheduled:
|
||||
reason = RefreshReason.Scheduled;
|
||||
break;
|
||||
}
|
||||
const refreshSessionPerCoin: (RefreshSessionRecord | undefined)[] =
|
||||
[];
|
||||
for (const oldCoin of backupRefreshGroup.old_coins) {
|
||||
const c = await tx.coins.get(oldCoin.coin_pub);
|
||||
checkBackupInvariant(!!c);
|
||||
const d = await tx.denominations.get([
|
||||
c.exchangeBaseUrl,
|
||||
c.denomPubHash,
|
||||
]);
|
||||
checkBackupInvariant(!!d);
|
||||
|
||||
if (oldCoin.refresh_session) {
|
||||
const denomSel = await getDenomSelStateFromBackup(
|
||||
tx,
|
||||
d.currency,
|
||||
c.exchangeBaseUrl,
|
||||
oldCoin.refresh_session.new_denoms,
|
||||
);
|
||||
refreshSessionPerCoin.push({
|
||||
sessionSecretSeed: oldCoin.refresh_session.session_secret_seed,
|
||||
norevealIndex: oldCoin.refresh_session.noreveal_index,
|
||||
newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({
|
||||
count: x.count,
|
||||
denomPubHash: x.denom_pub_hash,
|
||||
})),
|
||||
amountRefreshOutput: Amounts.stringify(denomSel.totalCoinValue),
|
||||
});
|
||||
} else {
|
||||
refreshSessionPerCoin.push(undefined);
|
||||
}
|
||||
}
|
||||
await tx.refreshGroups.put({
|
||||
timestampFinished: backupRefreshGroup.timestamp_finish,
|
||||
timestampCreated: backupRefreshGroup.timestamp_created,
|
||||
refreshGroupId: backupRefreshGroup.refresh_group_id,
|
||||
currency: Amounts.currencyOf(
|
||||
backupRefreshGroup.old_coins[0].input_amount,
|
||||
),
|
||||
reason,
|
||||
lastErrorPerCoin: {},
|
||||
oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
|
||||
statusPerCoin: backupRefreshGroup.old_coins.map((x) =>
|
||||
x.finished
|
||||
? RefreshCoinStatus.Finished
|
||||
: RefreshCoinStatus.Pending,
|
||||
),
|
||||
operationStatus: backupRefreshGroup.timestamp_finish
|
||||
? RefreshOperationStatus.Finished
|
||||
: RefreshOperationStatus.Pending,
|
||||
inputPerCoin: backupRefreshGroup.old_coins.map(
|
||||
(x) => x.input_amount,
|
||||
),
|
||||
estimatedOutputPerCoin: backupRefreshGroup.old_coins.map(
|
||||
(x) => x.estimated_output_amount,
|
||||
),
|
||||
refreshSessionPerCoin,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const backupTip of backupBlob.tips) {
|
||||
const ts = constructTombstone({
|
||||
tag: TombstoneTag.DeleteReward,
|
||||
walletTipId: backupTip.wallet_tip_id,
|
||||
});
|
||||
if (tombstoneSet.has(ts)) {
|
||||
continue;
|
||||
}
|
||||
const existingTip = await tx.rewards.get(backupTip.wallet_tip_id);
|
||||
if (!existingTip) {
|
||||
const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw);
|
||||
const denomsSel = await getDenomSelStateFromBackup(
|
||||
tx,
|
||||
tipAmountRaw.currency,
|
||||
backupTip.exchange_base_url,
|
||||
backupTip.selected_denoms,
|
||||
);
|
||||
await tx.rewards.put({
|
||||
acceptedTimestamp: backupTip.timestamp_accepted,
|
||||
createdTimestamp: backupTip.timestamp_created,
|
||||
denomsSel,
|
||||
next_url: backupTip.next_url,
|
||||
exchangeBaseUrl: backupTip.exchange_base_url,
|
||||
merchantBaseUrl: backupTip.exchange_base_url,
|
||||
merchantRewardId: backupTip.merchant_tip_id,
|
||||
pickedUpTimestamp: backupTip.timestamp_finished,
|
||||
secretSeed: backupTip.secret_seed,
|
||||
rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue),
|
||||
rewardAmountRaw: Amounts.stringify(tipAmountRaw),
|
||||
rewardExpiration: backupTip.timestamp_expiration,
|
||||
walletRewardId: backupTip.wallet_tip_id,
|
||||
denomSelUid: backupTip.selected_denoms_uid,
|
||||
status: RewardRecordStatus.Done, // FIXME!
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We now process tombstones.
|
||||
// The import code above should already prevent
|
||||
// importing things that are tombstoned,
|
||||
// but we do tombstone processing last just to be sure.
|
||||
|
||||
for (const tombstone of tombstoneSet) {
|
||||
const [type, ...rest] = tombstone.split(":");
|
||||
if (type === TombstoneTag.DeleteDepositGroup) {
|
||||
await tx.depositGroups.delete(rest[0]);
|
||||
} else if (type === TombstoneTag.DeletePayment) {
|
||||
await tx.purchases.delete(rest[0]);
|
||||
} else if (type === TombstoneTag.DeleteRefreshGroup) {
|
||||
await tx.refreshGroups.delete(rest[0]);
|
||||
} else if (type === TombstoneTag.DeleteRefund) {
|
||||
// Nothing required, will just prevent display
|
||||
// in the transactions list
|
||||
} else if (type === TombstoneTag.DeleteReward) {
|
||||
await tx.rewards.delete(rest[0]);
|
||||
} else if (type === TombstoneTag.DeleteWithdrawalGroup) {
|
||||
await tx.withdrawalGroups.delete(rest[0]);
|
||||
} else {
|
||||
logger.warn(`unable to process tombstone of type '${type}'`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@ -43,7 +43,6 @@ import {
|
||||
TalerErrorDetail,
|
||||
TalerPreciseTimestamp,
|
||||
URL,
|
||||
WalletBackupContentV1,
|
||||
buildCodecForObject,
|
||||
buildCodecForUnion,
|
||||
bytesToString,
|
||||
@ -99,9 +98,8 @@ import {
|
||||
TaskIdentifiers,
|
||||
} from "../common.js";
|
||||
import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js";
|
||||
import { exportBackup } from "./export.js";
|
||||
import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
|
||||
import { getWalletBackupState, provideBackupState } from "./state.js";
|
||||
import { WalletStoresV1 } from "../../db.js";
|
||||
import { GetReadOnlyAccess } from "../../util/query.js";
|
||||
|
||||
const logger = new Logger("operations/backup.ts");
|
||||
|
||||
@ -131,7 +129,7 @@ const magic = "TLRWBK01";
|
||||
*/
|
||||
export async function encryptBackup(
|
||||
config: WalletBackupConfState,
|
||||
blob: WalletBackupContentV1,
|
||||
blob: any,
|
||||
): Promise<Uint8Array> {
|
||||
const chunks: Uint8Array[] = [];
|
||||
chunks.push(stringToBytes(magic));
|
||||
@ -150,64 +148,6 @@ export async function encryptBackup(
|
||||
return concatArrays(chunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute cryptographic values for a backup blob.
|
||||
*
|
||||
* FIXME: Take data that we already know from the DB.
|
||||
* FIXME: Move computations into crypto worker.
|
||||
*/
|
||||
async function computeBackupCryptoData(
|
||||
cryptoApi: TalerCryptoInterface,
|
||||
backupContent: WalletBackupContentV1,
|
||||
): Promise<BackupCryptoPrecomputedData> {
|
||||
const cryptoData: BackupCryptoPrecomputedData = {
|
||||
coinPrivToCompletedCoin: {},
|
||||
rsaDenomPubToHash: {},
|
||||
proposalIdToContractTermsHash: {},
|
||||
proposalNoncePrivToPub: {},
|
||||
reservePrivToPub: {},
|
||||
};
|
||||
for (const backupExchangeDetails of backupContent.exchange_details) {
|
||||
for (const backupDenom of backupExchangeDetails.denominations) {
|
||||
if (backupDenom.denom_pub.cipher !== DenomKeyType.Rsa) {
|
||||
throw Error("unsupported cipher");
|
||||
}
|
||||
for (const backupCoin of backupDenom.coins) {
|
||||
const coinPub = encodeCrock(
|
||||
eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
|
||||
);
|
||||
const blindedCoin = rsaBlind(
|
||||
hash(decodeCrock(backupCoin.coin_priv)),
|
||||
decodeCrock(backupCoin.blinding_key),
|
||||
decodeCrock(backupDenom.denom_pub.rsa_public_key),
|
||||
);
|
||||
cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = {
|
||||
coinEvHash: encodeCrock(hash(blindedCoin)),
|
||||
coinPub,
|
||||
};
|
||||
}
|
||||
cryptoData.rsaDenomPubToHash[backupDenom.denom_pub.rsa_public_key] =
|
||||
encodeCrock(hashDenomPub(backupDenom.denom_pub));
|
||||
}
|
||||
}
|
||||
for (const backupWg of backupContent.withdrawal_groups) {
|
||||
cryptoData.reservePrivToPub[backupWg.reserve_priv] = encodeCrock(
|
||||
eddsaGetPublic(decodeCrock(backupWg.reserve_priv)),
|
||||
);
|
||||
}
|
||||
for (const purch of backupContent.purchases) {
|
||||
if (!purch.contract_terms_raw) continue;
|
||||
const { h: contractTermsHash } = await cryptoApi.hashString({
|
||||
str: canonicalJson(purch.contract_terms_raw),
|
||||
});
|
||||
const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(purch.nonce_priv)));
|
||||
cryptoData.proposalNoncePrivToPub[purch.nonce_priv] = noncePub;
|
||||
cryptoData.proposalIdToContractTermsHash[purch.proposal_id] =
|
||||
contractTermsHash;
|
||||
}
|
||||
return cryptoData;
|
||||
}
|
||||
|
||||
function deriveAccountKeyPair(
|
||||
bc: WalletBackupConfState,
|
||||
providerUrl: string,
|
||||
@ -262,7 +202,9 @@ async function runBackupCycleForProvider(
|
||||
return TaskRunResult.finished();
|
||||
}
|
||||
|
||||
const backupJson = await exportBackup(ws);
|
||||
//const backupJson = await exportBackup(ws);
|
||||
// FIXME: re-implement backup
|
||||
const backupJson = {};
|
||||
const backupConfig = await provideBackupState(ws);
|
||||
const encBackup = await encryptBackup(backupConfig, backupJson);
|
||||
const currentBackupHash = hash(encBackup);
|
||||
@ -441,9 +383,9 @@ async function runBackupCycleForProvider(
|
||||
logger.info("conflicting backup found");
|
||||
const backupEnc = new Uint8Array(await resp.bytes());
|
||||
const backupConfig = await provideBackupState(ws);
|
||||
const blob = await decryptBackup(backupConfig, backupEnc);
|
||||
const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
|
||||
await importBackup(ws, blob, cryptoData);
|
||||
// const blob = await decryptBackup(backupConfig, backupEnc);
|
||||
// FIXME: Re-implement backup import with merging
|
||||
// await importBackup(ws, blob, cryptoData);
|
||||
await ws.db
|
||||
.mktx((x) => [x.backupProviders, x.operationRetries])
|
||||
.runReadWrite(async (tx) => {
|
||||
@ -789,18 +731,6 @@ export interface BackupInfo {
|
||||
providers: ProviderInfo[];
|
||||
}
|
||||
|
||||
export async function importBackupPlain(
|
||||
ws: InternalWalletState,
|
||||
blob: any,
|
||||
): Promise<void> {
|
||||
// FIXME: parse
|
||||
const backup: WalletBackupContentV1 = blob;
|
||||
|
||||
const cryptoData = await computeBackupCryptoData(ws.cryptoApi, backup);
|
||||
|
||||
await importBackup(ws, blob, cryptoData);
|
||||
}
|
||||
|
||||
export enum ProviderPaymentType {
|
||||
Unpaid = "unpaid",
|
||||
Pending = "pending",
|
||||
@ -1036,23 +966,10 @@ export async function loadBackupRecovery(
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportBackupEncrypted(
|
||||
ws: InternalWalletState,
|
||||
): Promise<Uint8Array> {
|
||||
await provideBackupState(ws);
|
||||
const blob = await exportBackup(ws);
|
||||
const bs = await ws.db
|
||||
.mktx((x) => [x.config])
|
||||
.runReadOnly(async (tx) => {
|
||||
return await getWalletBackupState(ws, tx);
|
||||
});
|
||||
return encryptBackup(bs, blob);
|
||||
}
|
||||
|
||||
export async function decryptBackup(
|
||||
backupConfig: WalletBackupConfState,
|
||||
data: Uint8Array,
|
||||
): Promise<WalletBackupContentV1> {
|
||||
): Promise<any> {
|
||||
const rMagic = bytesToString(data.slice(0, 8));
|
||||
if (rMagic != magic) {
|
||||
throw Error("invalid backup file (magic tag mismatch)");
|
||||
@ -1068,12 +985,85 @@ export async function decryptBackup(
|
||||
return JSON.parse(bytesToString(gunzipSync(dataCompressed)));
|
||||
}
|
||||
|
||||
export async function importBackupEncrypted(
|
||||
export async function provideBackupState(
|
||||
ws: InternalWalletState,
|
||||
data: Uint8Array,
|
||||
): Promise<void> {
|
||||
const backupConfig = await provideBackupState(ws);
|
||||
const blob = await decryptBackup(backupConfig, data);
|
||||
const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
|
||||
await importBackup(ws, blob, cryptoData);
|
||||
): Promise<WalletBackupConfState> {
|
||||
const bs: ConfigRecord | undefined = await ws.db
|
||||
.mktx((stores) => [stores.config])
|
||||
.runReadOnly(async (tx) => {
|
||||
return await tx.config.get(ConfigRecordKey.WalletBackupState);
|
||||
});
|
||||
if (bs) {
|
||||
checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
|
||||
return bs.value;
|
||||
}
|
||||
// We need to generate the key outside of the transaction
|
||||
// due to how IndexedDB works.
|
||||
const k = await ws.cryptoApi.createEddsaKeypair({});
|
||||
const d = getRandomBytes(5);
|
||||
// FIXME: device ID should be configured when wallet is initialized
|
||||
// and be based on hostname
|
||||
const deviceId = `wallet-core-${encodeCrock(d)}`;
|
||||
return await ws.db
|
||||
.mktx((x) => [x.config])
|
||||
.runReadWrite(async (tx) => {
|
||||
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
|
||||
ConfigRecordKey.WalletBackupState,
|
||||
);
|
||||
if (!backupStateEntry) {
|
||||
backupStateEntry = {
|
||||
key: ConfigRecordKey.WalletBackupState,
|
||||
value: {
|
||||
deviceId,
|
||||
walletRootPub: k.pub,
|
||||
walletRootPriv: k.priv,
|
||||
lastBackupPlainHash: undefined,
|
||||
},
|
||||
};
|
||||
await tx.config.put(backupStateEntry);
|
||||
}
|
||||
checkDbInvariant(
|
||||
backupStateEntry.key === ConfigRecordKey.WalletBackupState,
|
||||
);
|
||||
return backupStateEntry.value;
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWalletBackupState(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
|
||||
): Promise<WalletBackupConfState> {
|
||||
const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
|
||||
checkDbInvariant(!!bs, "wallet backup state should be in DB");
|
||||
checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
|
||||
return bs.value;
|
||||
}
|
||||
|
||||
export async function setWalletDeviceId(
|
||||
ws: InternalWalletState,
|
||||
deviceId: string,
|
||||
): Promise<void> {
|
||||
await provideBackupState(ws);
|
||||
await ws.db
|
||||
.mktx((x) => [x.config])
|
||||
.runReadWrite(async (tx) => {
|
||||
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
|
||||
ConfigRecordKey.WalletBackupState,
|
||||
);
|
||||
if (
|
||||
!backupStateEntry ||
|
||||
backupStateEntry.key !== ConfigRecordKey.WalletBackupState
|
||||
) {
|
||||
return;
|
||||
}
|
||||
backupStateEntry.value.deviceId = deviceId;
|
||||
await tx.config.put(backupStateEntry);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWalletDeviceId(
|
||||
ws: InternalWalletState,
|
||||
): Promise<string> {
|
||||
const bs = await provideBackupState(ws);
|
||||
return bs.deviceId;
|
||||
}
|
||||
|
@ -14,96 +14,4 @@
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
ConfigRecord,
|
||||
ConfigRecordKey,
|
||||
WalletBackupConfState,
|
||||
WalletStoresV1,
|
||||
} from "../../db.js";
|
||||
import { checkDbInvariant } from "../../util/invariants.js";
|
||||
import { GetReadOnlyAccess } from "../../util/query.js";
|
||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||
|
||||
export async function provideBackupState(
|
||||
ws: InternalWalletState,
|
||||
): Promise<WalletBackupConfState> {
|
||||
const bs: ConfigRecord | undefined = await ws.db
|
||||
.mktx((stores) => [stores.config])
|
||||
.runReadOnly(async (tx) => {
|
||||
return await tx.config.get(ConfigRecordKey.WalletBackupState);
|
||||
});
|
||||
if (bs) {
|
||||
checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
|
||||
return bs.value;
|
||||
}
|
||||
// We need to generate the key outside of the transaction
|
||||
// due to how IndexedDB works.
|
||||
const k = await ws.cryptoApi.createEddsaKeypair({});
|
||||
const d = getRandomBytes(5);
|
||||
// FIXME: device ID should be configured when wallet is initialized
|
||||
// and be based on hostname
|
||||
const deviceId = `wallet-core-${encodeCrock(d)}`;
|
||||
return await ws.db
|
||||
.mktx((x) => [x.config])
|
||||
.runReadWrite(async (tx) => {
|
||||
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
|
||||
ConfigRecordKey.WalletBackupState,
|
||||
);
|
||||
if (!backupStateEntry) {
|
||||
backupStateEntry = {
|
||||
key: ConfigRecordKey.WalletBackupState,
|
||||
value: {
|
||||
deviceId,
|
||||
walletRootPub: k.pub,
|
||||
walletRootPriv: k.priv,
|
||||
lastBackupPlainHash: undefined,
|
||||
},
|
||||
};
|
||||
await tx.config.put(backupStateEntry);
|
||||
}
|
||||
checkDbInvariant(
|
||||
backupStateEntry.key === ConfigRecordKey.WalletBackupState,
|
||||
);
|
||||
return backupStateEntry.value;
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWalletBackupState(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
|
||||
): Promise<WalletBackupConfState> {
|
||||
const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
|
||||
checkDbInvariant(!!bs, "wallet backup state should be in DB");
|
||||
checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
|
||||
return bs.value;
|
||||
}
|
||||
|
||||
export async function setWalletDeviceId(
|
||||
ws: InternalWalletState,
|
||||
deviceId: string,
|
||||
): Promise<void> {
|
||||
await provideBackupState(ws);
|
||||
await ws.db
|
||||
.mktx((x) => [x.config])
|
||||
.runReadWrite(async (tx) => {
|
||||
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
|
||||
ConfigRecordKey.WalletBackupState,
|
||||
);
|
||||
if (
|
||||
!backupStateEntry ||
|
||||
backupStateEntry.key !== ConfigRecordKey.WalletBackupState
|
||||
) {
|
||||
return;
|
||||
}
|
||||
backupStateEntry.value.deviceId = deviceId;
|
||||
await tx.config.put(backupStateEntry);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWalletDeviceId(
|
||||
ws: InternalWalletState,
|
||||
): Promise<string> {
|
||||
const bs = await provideBackupState(ws);
|
||||
return bs.deviceId;
|
||||
}
|
||||
|
@ -106,7 +106,6 @@ import {
|
||||
UserAttentionsResponse,
|
||||
ValidateIbanRequest,
|
||||
ValidateIbanResponse,
|
||||
WalletBackupContentV1,
|
||||
WalletCoreVersion,
|
||||
WalletCurrencyInfo,
|
||||
WithdrawFakebankRequest,
|
||||
@ -116,6 +115,10 @@ import {
|
||||
SharePaymentResult,
|
||||
GetCurrencyInfoRequest,
|
||||
GetCurrencyInfoResponse,
|
||||
StoredBackupList,
|
||||
CreateStoredBackupResponse,
|
||||
RecoverStoredBackupRequest,
|
||||
DeleteStoredBackupRequest,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { AuditorTrustRecord, WalletContractData } from "./db.js";
|
||||
import {
|
||||
@ -195,7 +198,6 @@ export enum WalletApiOperation {
|
||||
GenerateDepositGroupTxId = "generateDepositGroupTxId",
|
||||
CreateDepositGroup = "createDepositGroup",
|
||||
SetWalletDeviceId = "setWalletDeviceId",
|
||||
ExportBackupPlain = "exportBackupPlain",
|
||||
WithdrawFakebank = "withdrawFakebank",
|
||||
ImportDb = "importDb",
|
||||
ExportDb = "exportDb",
|
||||
@ -214,6 +216,10 @@ export enum WalletApiOperation {
|
||||
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
|
||||
TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
|
||||
GetScopedCurrencyInfo = "getScopedCurrencyInfo",
|
||||
ListStoredBackups = "listStoredBackups",
|
||||
CreateStoredBackup = "createStoredBackup",
|
||||
DeleteStoredBackup = "deleteStoredBackup",
|
||||
RecoverStoredBackup = "recoverStoredBackup",
|
||||
}
|
||||
|
||||
// group: Initialization
|
||||
@ -713,13 +719,28 @@ export type SetWalletDeviceIdOp = {
|
||||
response: EmptyObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* Export a backup JSON, mostly useful for testing.
|
||||
*/
|
||||
export type ExportBackupPlainOp = {
|
||||
op: WalletApiOperation.ExportBackupPlain;
|
||||
export type ListStoredBackupsOp = {
|
||||
op: WalletApiOperation.ListStoredBackups;
|
||||
request: EmptyObject;
|
||||
response: WalletBackupContentV1;
|
||||
response: StoredBackupList;
|
||||
};
|
||||
|
||||
export type CreateStoredBackupsOp = {
|
||||
op: WalletApiOperation.CreateStoredBackup;
|
||||
request: EmptyObject;
|
||||
response: CreateStoredBackupResponse;
|
||||
};
|
||||
|
||||
export type RecoverStoredBackupsOp = {
|
||||
op: WalletApiOperation.RecoverStoredBackup;
|
||||
request: RecoverStoredBackupRequest;
|
||||
response: EmptyObject;
|
||||
};
|
||||
|
||||
export type DeleteStoredBackupOp = {
|
||||
op: WalletApiOperation.DeleteStoredBackup;
|
||||
request: DeleteStoredBackupRequest;
|
||||
response: EmptyObject;
|
||||
};
|
||||
|
||||
// group: Peer Payments
|
||||
@ -1062,7 +1083,6 @@ export type WalletOperations = {
|
||||
[WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp;
|
||||
[WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp;
|
||||
[WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp;
|
||||
[WalletApiOperation.ExportBackupPlain]: ExportBackupPlainOp;
|
||||
[WalletApiOperation.ExportBackupRecovery]: ExportBackupRecoveryOp;
|
||||
[WalletApiOperation.ImportBackupRecovery]: ImportBackupRecoveryOp;
|
||||
[WalletApiOperation.RunBackupCycle]: RunBackupCycleOp;
|
||||
@ -1092,6 +1112,10 @@ export type WalletOperations = {
|
||||
[WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal;
|
||||
[WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal;
|
||||
[WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp;
|
||||
[WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp;
|
||||
[WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp;
|
||||
[WalletApiOperation.DeleteStoredBackup]: DeleteStoredBackupOp;
|
||||
[WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp;
|
||||
};
|
||||
|
||||
export type WalletCoreRequestType<
|
||||
|
@ -120,6 +120,7 @@ import {
|
||||
codecForSharePaymentRequest,
|
||||
GetCurrencyInfoResponse,
|
||||
codecForGetCurrencyInfoRequest,
|
||||
CreateStoredBackupResponse,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
HttpRequestLibrary,
|
||||
@ -139,6 +140,7 @@ import {
|
||||
clearDatabase,
|
||||
exportDb,
|
||||
importDb,
|
||||
openStoredBackupsDatabase,
|
||||
openTalerDatabase,
|
||||
} from "./db.js";
|
||||
import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
|
||||
@ -158,7 +160,6 @@ import {
|
||||
getUserAttentionsUnreadCount,
|
||||
markAttentionRequestAsRead,
|
||||
} from "./operations/attention.js";
|
||||
import { exportBackup } from "./operations/backup/export.js";
|
||||
import {
|
||||
addBackupProvider,
|
||||
codecForAddBackupProviderRequest,
|
||||
@ -166,13 +167,12 @@ import {
|
||||
codecForRunBackupCycle,
|
||||
getBackupInfo,
|
||||
getBackupRecovery,
|
||||
importBackupPlain,
|
||||
loadBackupRecovery,
|
||||
processBackupForProvider,
|
||||
removeBackupProvider,
|
||||
runBackupCycle,
|
||||
setWalletDeviceId,
|
||||
} from "./operations/backup/index.js";
|
||||
import { setWalletDeviceId } from "./operations/backup/state.js";
|
||||
import { getBalanceDetail, getBalances } from "./operations/balance.js";
|
||||
import {
|
||||
TaskIdentifiers,
|
||||
@ -1025,6 +1025,17 @@ export async function getClientFromWalletState(
|
||||
return client;
|
||||
}
|
||||
|
||||
async function createStoredBackup(
|
||||
ws: InternalWalletState,
|
||||
): Promise<CreateStoredBackupResponse> {
|
||||
const backup = await exportDb(ws.idb);
|
||||
const backupsDb = await openStoredBackupsDatabase(ws.idb);
|
||||
const name = `backup-${new Date().getTime()}`;
|
||||
backupsDb.mktxAll().runReadWrite(async (tx) => {});
|
||||
|
||||
throw Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the "wallet-core" API.
|
||||
*/
|
||||
@ -1041,6 +1052,14 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
// FIXME: Can we make this more type-safe by using the request/response type
|
||||
// definitions we already have?
|
||||
switch (operation) {
|
||||
case WalletApiOperation.CreateStoredBackup:
|
||||
return createStoredBackup(ws);
|
||||
case WalletApiOperation.DeleteStoredBackup:
|
||||
return {};
|
||||
case WalletApiOperation.ListStoredBackups:
|
||||
return {};
|
||||
case WalletApiOperation.RecoverStoredBackup:
|
||||
return {};
|
||||
case WalletApiOperation.InitWallet: {
|
||||
logger.trace("initializing wallet");
|
||||
ws.initCalled = true;
|
||||
@ -1382,9 +1401,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
const req = codecForAcceptTipRequest().decode(payload);
|
||||
return await acceptTip(ws, req.walletRewardId);
|
||||
}
|
||||
case WalletApiOperation.ExportBackupPlain: {
|
||||
return exportBackup(ws);
|
||||
}
|
||||
case WalletApiOperation.AddBackupProvider: {
|
||||
const req = codecForAddBackupProviderRequest().decode(payload);
|
||||
return await addBackupProvider(ws, req);
|
||||
@ -1535,9 +1551,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
await clearDatabase(ws.db.idbHandle());
|
||||
return {};
|
||||
case WalletApiOperation.Recycle: {
|
||||
const backup = await exportBackup(ws);
|
||||
await clearDatabase(ws.db.idbHandle());
|
||||
await importBackupPlain(ws, backup);
|
||||
throw Error("not implemented");
|
||||
return {};
|
||||
}
|
||||
case WalletApiOperation.ExportDb: {
|
||||
|
@ -139,7 +139,7 @@ async function runGarbageCollector(): Promise<void> {
|
||||
if (!dbBeforeGc) {
|
||||
throw Error("no current db before running gc");
|
||||
}
|
||||
const dump = await exportDb(dbBeforeGc.idbHandle());
|
||||
const dump = await exportDb(indexedDB as any);
|
||||
|
||||
await deleteTalerDatabase(indexedDB as any);
|
||||
logger.info("cleaned");
|
||||
|
Loading…
Reference in New Issue
Block a user