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;
|
onlyState?: TransactionStateFilter;
|
||||||
onlyCurrency?: string;
|
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 {
|
export interface DbDumpRecord {
|
||||||
/**
|
/**
|
||||||
* Key, serialized with structuredEncapsulated.
|
* Key, serialized with structuredEncapsulated.
|
||||||
@ -2831,6 +2849,7 @@ export async function exportSingleDb(
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = myDb.transaction(Array.from(myDb.objectStoreNames));
|
const tx = myDb.transaction(Array.from(myDb.objectStoreNames));
|
||||||
tx.addEventListener("complete", () => {
|
tx.addEventListener("complete", () => {
|
||||||
|
myDb.close();
|
||||||
resolve(singleDbDump);
|
resolve(singleDbDump);
|
||||||
});
|
});
|
||||||
// tslint:disable-next-line:prefer-for-of
|
// 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
|
* Return a promise that resolves
|
||||||
* to the taler wallet db.
|
* 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,
|
TalerErrorDetail,
|
||||||
TalerPreciseTimestamp,
|
TalerPreciseTimestamp,
|
||||||
URL,
|
URL,
|
||||||
WalletBackupContentV1,
|
|
||||||
buildCodecForObject,
|
buildCodecForObject,
|
||||||
buildCodecForUnion,
|
buildCodecForUnion,
|
||||||
bytesToString,
|
bytesToString,
|
||||||
@ -99,9 +98,8 @@ import {
|
|||||||
TaskIdentifiers,
|
TaskIdentifiers,
|
||||||
} from "../common.js";
|
} from "../common.js";
|
||||||
import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js";
|
import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js";
|
||||||
import { exportBackup } from "./export.js";
|
import { WalletStoresV1 } from "../../db.js";
|
||||||
import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
|
import { GetReadOnlyAccess } from "../../util/query.js";
|
||||||
import { getWalletBackupState, provideBackupState } from "./state.js";
|
|
||||||
|
|
||||||
const logger = new Logger("operations/backup.ts");
|
const logger = new Logger("operations/backup.ts");
|
||||||
|
|
||||||
@ -131,7 +129,7 @@ const magic = "TLRWBK01";
|
|||||||
*/
|
*/
|
||||||
export async function encryptBackup(
|
export async function encryptBackup(
|
||||||
config: WalletBackupConfState,
|
config: WalletBackupConfState,
|
||||||
blob: WalletBackupContentV1,
|
blob: any,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const chunks: Uint8Array[] = [];
|
const chunks: Uint8Array[] = [];
|
||||||
chunks.push(stringToBytes(magic));
|
chunks.push(stringToBytes(magic));
|
||||||
@ -150,64 +148,6 @@ export async function encryptBackup(
|
|||||||
return concatArrays(chunks);
|
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(
|
function deriveAccountKeyPair(
|
||||||
bc: WalletBackupConfState,
|
bc: WalletBackupConfState,
|
||||||
providerUrl: string,
|
providerUrl: string,
|
||||||
@ -262,7 +202,9 @@ async function runBackupCycleForProvider(
|
|||||||
return TaskRunResult.finished();
|
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 backupConfig = await provideBackupState(ws);
|
||||||
const encBackup = await encryptBackup(backupConfig, backupJson);
|
const encBackup = await encryptBackup(backupConfig, backupJson);
|
||||||
const currentBackupHash = hash(encBackup);
|
const currentBackupHash = hash(encBackup);
|
||||||
@ -441,9 +383,9 @@ async function runBackupCycleForProvider(
|
|||||||
logger.info("conflicting backup found");
|
logger.info("conflicting backup found");
|
||||||
const backupEnc = new Uint8Array(await resp.bytes());
|
const backupEnc = new Uint8Array(await resp.bytes());
|
||||||
const backupConfig = await provideBackupState(ws);
|
const backupConfig = await provideBackupState(ws);
|
||||||
const blob = await decryptBackup(backupConfig, backupEnc);
|
// const blob = await decryptBackup(backupConfig, backupEnc);
|
||||||
const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
|
// FIXME: Re-implement backup import with merging
|
||||||
await importBackup(ws, blob, cryptoData);
|
// await importBackup(ws, blob, cryptoData);
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => [x.backupProviders, x.operationRetries])
|
.mktx((x) => [x.backupProviders, x.operationRetries])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
@ -789,18 +731,6 @@ export interface BackupInfo {
|
|||||||
providers: ProviderInfo[];
|
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 {
|
export enum ProviderPaymentType {
|
||||||
Unpaid = "unpaid",
|
Unpaid = "unpaid",
|
||||||
Pending = "pending",
|
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(
|
export async function decryptBackup(
|
||||||
backupConfig: WalletBackupConfState,
|
backupConfig: WalletBackupConfState,
|
||||||
data: Uint8Array,
|
data: Uint8Array,
|
||||||
): Promise<WalletBackupContentV1> {
|
): Promise<any> {
|
||||||
const rMagic = bytesToString(data.slice(0, 8));
|
const rMagic = bytesToString(data.slice(0, 8));
|
||||||
if (rMagic != magic) {
|
if (rMagic != magic) {
|
||||||
throw Error("invalid backup file (magic tag mismatch)");
|
throw Error("invalid backup file (magic tag mismatch)");
|
||||||
@ -1068,12 +985,85 @@ export async function decryptBackup(
|
|||||||
return JSON.parse(bytesToString(gunzipSync(dataCompressed)));
|
return JSON.parse(bytesToString(gunzipSync(dataCompressed)));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importBackupEncrypted(
|
export async function provideBackupState(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
data: Uint8Array,
|
): Promise<WalletBackupConfState> {
|
||||||
): Promise<void> {
|
const bs: ConfigRecord | undefined = await ws.db
|
||||||
const backupConfig = await provideBackupState(ws);
|
.mktx((stores) => [stores.config])
|
||||||
const blob = await decryptBackup(backupConfig, data);
|
.runReadOnly(async (tx) => {
|
||||||
const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
|
return await tx.config.get(ConfigRecordKey.WalletBackupState);
|
||||||
await importBackup(ws, blob, cryptoData);
|
});
|
||||||
|
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/>
|
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,
|
UserAttentionsResponse,
|
||||||
ValidateIbanRequest,
|
ValidateIbanRequest,
|
||||||
ValidateIbanResponse,
|
ValidateIbanResponse,
|
||||||
WalletBackupContentV1,
|
|
||||||
WalletCoreVersion,
|
WalletCoreVersion,
|
||||||
WalletCurrencyInfo,
|
WalletCurrencyInfo,
|
||||||
WithdrawFakebankRequest,
|
WithdrawFakebankRequest,
|
||||||
@ -116,6 +115,10 @@ import {
|
|||||||
SharePaymentResult,
|
SharePaymentResult,
|
||||||
GetCurrencyInfoRequest,
|
GetCurrencyInfoRequest,
|
||||||
GetCurrencyInfoResponse,
|
GetCurrencyInfoResponse,
|
||||||
|
StoredBackupList,
|
||||||
|
CreateStoredBackupResponse,
|
||||||
|
RecoverStoredBackupRequest,
|
||||||
|
DeleteStoredBackupRequest,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { AuditorTrustRecord, WalletContractData } from "./db.js";
|
import { AuditorTrustRecord, WalletContractData } from "./db.js";
|
||||||
import {
|
import {
|
||||||
@ -195,7 +198,6 @@ export enum WalletApiOperation {
|
|||||||
GenerateDepositGroupTxId = "generateDepositGroupTxId",
|
GenerateDepositGroupTxId = "generateDepositGroupTxId",
|
||||||
CreateDepositGroup = "createDepositGroup",
|
CreateDepositGroup = "createDepositGroup",
|
||||||
SetWalletDeviceId = "setWalletDeviceId",
|
SetWalletDeviceId = "setWalletDeviceId",
|
||||||
ExportBackupPlain = "exportBackupPlain",
|
|
||||||
WithdrawFakebank = "withdrawFakebank",
|
WithdrawFakebank = "withdrawFakebank",
|
||||||
ImportDb = "importDb",
|
ImportDb = "importDb",
|
||||||
ExportDb = "exportDb",
|
ExportDb = "exportDb",
|
||||||
@ -214,6 +216,10 @@ export enum WalletApiOperation {
|
|||||||
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
|
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
|
||||||
TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
|
TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
|
||||||
GetScopedCurrencyInfo = "getScopedCurrencyInfo",
|
GetScopedCurrencyInfo = "getScopedCurrencyInfo",
|
||||||
|
ListStoredBackups = "listStoredBackups",
|
||||||
|
CreateStoredBackup = "createStoredBackup",
|
||||||
|
DeleteStoredBackup = "deleteStoredBackup",
|
||||||
|
RecoverStoredBackup = "recoverStoredBackup",
|
||||||
}
|
}
|
||||||
|
|
||||||
// group: Initialization
|
// group: Initialization
|
||||||
@ -713,13 +719,28 @@ export type SetWalletDeviceIdOp = {
|
|||||||
response: EmptyObject;
|
response: EmptyObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export type ListStoredBackupsOp = {
|
||||||
* Export a backup JSON, mostly useful for testing.
|
op: WalletApiOperation.ListStoredBackups;
|
||||||
*/
|
|
||||||
export type ExportBackupPlainOp = {
|
|
||||||
op: WalletApiOperation.ExportBackupPlain;
|
|
||||||
request: EmptyObject;
|
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
|
// group: Peer Payments
|
||||||
@ -1062,7 +1083,6 @@ export type WalletOperations = {
|
|||||||
[WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp;
|
[WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp;
|
||||||
[WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp;
|
[WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp;
|
||||||
[WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp;
|
[WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp;
|
||||||
[WalletApiOperation.ExportBackupPlain]: ExportBackupPlainOp;
|
|
||||||
[WalletApiOperation.ExportBackupRecovery]: ExportBackupRecoveryOp;
|
[WalletApiOperation.ExportBackupRecovery]: ExportBackupRecoveryOp;
|
||||||
[WalletApiOperation.ImportBackupRecovery]: ImportBackupRecoveryOp;
|
[WalletApiOperation.ImportBackupRecovery]: ImportBackupRecoveryOp;
|
||||||
[WalletApiOperation.RunBackupCycle]: RunBackupCycleOp;
|
[WalletApiOperation.RunBackupCycle]: RunBackupCycleOp;
|
||||||
@ -1092,6 +1112,10 @@ export type WalletOperations = {
|
|||||||
[WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal;
|
[WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal;
|
||||||
[WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal;
|
[WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal;
|
||||||
[WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp;
|
[WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp;
|
||||||
|
[WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp;
|
||||||
|
[WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp;
|
||||||
|
[WalletApiOperation.DeleteStoredBackup]: DeleteStoredBackupOp;
|
||||||
|
[WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WalletCoreRequestType<
|
export type WalletCoreRequestType<
|
||||||
|
@ -120,6 +120,7 @@ import {
|
|||||||
codecForSharePaymentRequest,
|
codecForSharePaymentRequest,
|
||||||
GetCurrencyInfoResponse,
|
GetCurrencyInfoResponse,
|
||||||
codecForGetCurrencyInfoRequest,
|
codecForGetCurrencyInfoRequest,
|
||||||
|
CreateStoredBackupResponse,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
HttpRequestLibrary,
|
HttpRequestLibrary,
|
||||||
@ -139,6 +140,7 @@ import {
|
|||||||
clearDatabase,
|
clearDatabase,
|
||||||
exportDb,
|
exportDb,
|
||||||
importDb,
|
importDb,
|
||||||
|
openStoredBackupsDatabase,
|
||||||
openTalerDatabase,
|
openTalerDatabase,
|
||||||
} from "./db.js";
|
} from "./db.js";
|
||||||
import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
|
import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
|
||||||
@ -158,7 +160,6 @@ import {
|
|||||||
getUserAttentionsUnreadCount,
|
getUserAttentionsUnreadCount,
|
||||||
markAttentionRequestAsRead,
|
markAttentionRequestAsRead,
|
||||||
} from "./operations/attention.js";
|
} from "./operations/attention.js";
|
||||||
import { exportBackup } from "./operations/backup/export.js";
|
|
||||||
import {
|
import {
|
||||||
addBackupProvider,
|
addBackupProvider,
|
||||||
codecForAddBackupProviderRequest,
|
codecForAddBackupProviderRequest,
|
||||||
@ -166,13 +167,12 @@ import {
|
|||||||
codecForRunBackupCycle,
|
codecForRunBackupCycle,
|
||||||
getBackupInfo,
|
getBackupInfo,
|
||||||
getBackupRecovery,
|
getBackupRecovery,
|
||||||
importBackupPlain,
|
|
||||||
loadBackupRecovery,
|
loadBackupRecovery,
|
||||||
processBackupForProvider,
|
processBackupForProvider,
|
||||||
removeBackupProvider,
|
removeBackupProvider,
|
||||||
runBackupCycle,
|
runBackupCycle,
|
||||||
|
setWalletDeviceId,
|
||||||
} from "./operations/backup/index.js";
|
} from "./operations/backup/index.js";
|
||||||
import { setWalletDeviceId } from "./operations/backup/state.js";
|
|
||||||
import { getBalanceDetail, getBalances } from "./operations/balance.js";
|
import { getBalanceDetail, getBalances } from "./operations/balance.js";
|
||||||
import {
|
import {
|
||||||
TaskIdentifiers,
|
TaskIdentifiers,
|
||||||
@ -1025,6 +1025,17 @@ export async function getClientFromWalletState(
|
|||||||
return client;
|
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.
|
* 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
|
// FIXME: Can we make this more type-safe by using the request/response type
|
||||||
// definitions we already have?
|
// definitions we already have?
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
|
case WalletApiOperation.CreateStoredBackup:
|
||||||
|
return createStoredBackup(ws);
|
||||||
|
case WalletApiOperation.DeleteStoredBackup:
|
||||||
|
return {};
|
||||||
|
case WalletApiOperation.ListStoredBackups:
|
||||||
|
return {};
|
||||||
|
case WalletApiOperation.RecoverStoredBackup:
|
||||||
|
return {};
|
||||||
case WalletApiOperation.InitWallet: {
|
case WalletApiOperation.InitWallet: {
|
||||||
logger.trace("initializing wallet");
|
logger.trace("initializing wallet");
|
||||||
ws.initCalled = true;
|
ws.initCalled = true;
|
||||||
@ -1382,9 +1401,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
const req = codecForAcceptTipRequest().decode(payload);
|
const req = codecForAcceptTipRequest().decode(payload);
|
||||||
return await acceptTip(ws, req.walletRewardId);
|
return await acceptTip(ws, req.walletRewardId);
|
||||||
}
|
}
|
||||||
case WalletApiOperation.ExportBackupPlain: {
|
|
||||||
return exportBackup(ws);
|
|
||||||
}
|
|
||||||
case WalletApiOperation.AddBackupProvider: {
|
case WalletApiOperation.AddBackupProvider: {
|
||||||
const req = codecForAddBackupProviderRequest().decode(payload);
|
const req = codecForAddBackupProviderRequest().decode(payload);
|
||||||
return await addBackupProvider(ws, req);
|
return await addBackupProvider(ws, req);
|
||||||
@ -1535,9 +1551,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
await clearDatabase(ws.db.idbHandle());
|
await clearDatabase(ws.db.idbHandle());
|
||||||
return {};
|
return {};
|
||||||
case WalletApiOperation.Recycle: {
|
case WalletApiOperation.Recycle: {
|
||||||
const backup = await exportBackup(ws);
|
throw Error("not implemented");
|
||||||
await clearDatabase(ws.db.idbHandle());
|
|
||||||
await importBackupPlain(ws, backup);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
case WalletApiOperation.ExportDb: {
|
case WalletApiOperation.ExportDb: {
|
||||||
|
@ -139,7 +139,7 @@ async function runGarbageCollector(): Promise<void> {
|
|||||||
if (!dbBeforeGc) {
|
if (!dbBeforeGc) {
|
||||||
throw Error("no current db before running gc");
|
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);
|
await deleteTalerDatabase(indexedDB as any);
|
||||||
logger.info("cleaned");
|
logger.info("cleaned");
|
||||||
|
Loading…
Reference in New Issue
Block a user