Merge branch 'master' into age-withdraw
This commit is contained in:
commit
94cfcc8750
File diff suppressed because it is too large
Load Diff
@ -159,8 +159,8 @@ test("taler refund uri parsing with instance", (t) => {
|
||||
t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/myinst/");
|
||||
});
|
||||
|
||||
test("taler tip pickup uri", (t) => {
|
||||
const url1 = "taler://tip/merchant.example.com/tipid";
|
||||
test("taler reward pickup uri", (t) => {
|
||||
const url1 = "taler://reward/merchant.example.com/tipid";
|
||||
const r1 = parseRewardUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
@ -169,26 +169,26 @@ test("taler tip pickup uri", (t) => {
|
||||
t.is(r1.merchantBaseUrl, "https://merchant.example.com/");
|
||||
});
|
||||
|
||||
test("taler tip pickup uri with instance", (t) => {
|
||||
const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid";
|
||||
test("taler reward pickup uri with instance", (t) => {
|
||||
const url1 = "taler://reward/merchant.example.com/instances/tipm/tipid";
|
||||
const r1 = parseRewardUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/tipm/");
|
||||
t.is(r1.merchantTipId, "tipid");
|
||||
t.is(r1.merchantRewardId, "tipid");
|
||||
});
|
||||
|
||||
test("taler tip pickup uri with instance and prefix", (t) => {
|
||||
const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid";
|
||||
test("taler reward pickup uri with instance and prefix", (t) => {
|
||||
const url1 = "taler://reward/merchant.example.com/my/pfx/tipm/tipid";
|
||||
const r1 = parseRewardUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/");
|
||||
t.is(r1.merchantTipId, "tipid");
|
||||
t.is(r1.merchantRewardId, "tipid");
|
||||
});
|
||||
|
||||
test("taler peer to peer push URI", (t) => {
|
||||
|
@ -63,7 +63,7 @@ export interface RefundUriResult {
|
||||
export interface RewardUriResult {
|
||||
type: TalerUriAction.Reward;
|
||||
merchantBaseUrl: string;
|
||||
merchantTipId: string;
|
||||
merchantRewardId: string;
|
||||
}
|
||||
|
||||
export interface ExchangeUri {
|
||||
@ -408,7 +408,7 @@ export function parseRewardUri(s: string): RewardUriResult | undefined {
|
||||
return undefined;
|
||||
}
|
||||
const host = parts[0].toLowerCase();
|
||||
const tipId = parts[parts.length - 1];
|
||||
const rewardId = parts[parts.length - 1];
|
||||
const pathSegments = parts.slice(1, parts.length - 1);
|
||||
const hostAndSegments = [host, ...pathSegments].join("/");
|
||||
const merchantBaseUrl = canonicalizeBaseUrl(
|
||||
@ -418,7 +418,7 @@ export function parseRewardUri(s: string): RewardUriResult | undefined {
|
||||
return {
|
||||
type: TalerUriAction.Reward,
|
||||
merchantBaseUrl,
|
||||
merchantTipId: tipId,
|
||||
merchantRewardId: rewardId,
|
||||
};
|
||||
}
|
||||
|
||||
@ -701,10 +701,10 @@ export function stringifyRefundUri({
|
||||
}
|
||||
export function stringifyRewardUri({
|
||||
merchantBaseUrl,
|
||||
merchantTipId,
|
||||
merchantRewardId,
|
||||
}: Omit<RewardUriResult, "type">): string {
|
||||
const { proto, path } = getUrlInfo(merchantBaseUrl);
|
||||
return `${proto}://reward/${path}${merchantTipId}`;
|
||||
return `${proto}://reward/${path}${merchantRewardId}`;
|
||||
}
|
||||
|
||||
export function stringifyExchangeUri({
|
||||
|
@ -312,6 +312,14 @@ export namespace Duration {
|
||||
}
|
||||
|
||||
export namespace AbsoluteTime {
|
||||
export function getStampMsNow(): number {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
export function getStampMsNever(): number {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
export function now(): AbsoluteTime {
|
||||
return {
|
||||
t_ms: new Date().getTime() + timeshift,
|
||||
@ -398,6 +406,13 @@ export namespace AbsoluteTime {
|
||||
};
|
||||
}
|
||||
|
||||
export function fromStampMs(stampMs: number): AbsoluteTime {
|
||||
return {
|
||||
t_ms: stampMs,
|
||||
[opaque_AbsoluteTime]: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fromPreciseTimestamp(t: TalerPreciseTimestamp): AbsoluteTime {
|
||||
if (t.t_s === "never") {
|
||||
return { t_ms: "never", [opaque_AbsoluteTime]: true };
|
||||
@ -409,6 +424,13 @@ export namespace AbsoluteTime {
|
||||
};
|
||||
}
|
||||
|
||||
export function toStampMs(at: AbsoluteTime): number {
|
||||
if (at.t_ms === "never") {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
return at.t_ms;
|
||||
}
|
||||
|
||||
export function toPreciseTimestamp(at: AbsoluteTime): TalerPreciseTimestamp {
|
||||
if (at.t_ms == "never") {
|
||||
return {
|
||||
|
@ -1262,17 +1262,25 @@ export interface ExchangeFullDetails {
|
||||
}
|
||||
|
||||
export enum ExchangeTosStatus {
|
||||
New = "new",
|
||||
Pending = "pending",
|
||||
Proposed = "proposed",
|
||||
Accepted = "accepted",
|
||||
Changed = "changed",
|
||||
NotFound = "not-found",
|
||||
Unknown = "unknown",
|
||||
}
|
||||
|
||||
export enum ExchangeEntryStatus {
|
||||
Unknown = "unknown",
|
||||
Outdated = "outdated",
|
||||
Ok = "ok",
|
||||
Preset = "preset",
|
||||
Ephemeral = "ephemeral",
|
||||
Used = "used",
|
||||
}
|
||||
|
||||
export enum ExchangeUpdateStatus {
|
||||
Initial = "initial",
|
||||
InitialUpdate = "initial(update)",
|
||||
Suspended = "suspended",
|
||||
Failed = "failed",
|
||||
OutdatedUpdate = "outdated(update)",
|
||||
Ready = "ready",
|
||||
ReadyUpdate = "ready(update)",
|
||||
}
|
||||
|
||||
export interface OperationErrorInfo {
|
||||
@ -1285,13 +1293,9 @@ export interface ExchangeListItem {
|
||||
currency: string | undefined;
|
||||
paytoUris: string[];
|
||||
tosStatus: ExchangeTosStatus;
|
||||
exchangeStatus: ExchangeEntryStatus;
|
||||
exchangeEntryStatus: ExchangeEntryStatus;
|
||||
exchangeUpdateStatus: ExchangeUpdateStatus;
|
||||
ageRestrictionOptions: number[];
|
||||
/**
|
||||
* Permanently added to the wallet, as opposed to just
|
||||
* temporarily queried.
|
||||
*/
|
||||
permanent: boolean;
|
||||
|
||||
/**
|
||||
* Information about the last error that occurred when trying
|
||||
@ -1370,8 +1374,8 @@ export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
|
||||
.property("exchangeBaseUrl", codecForString())
|
||||
.property("paytoUris", codecForList(codecForString()))
|
||||
.property("tosStatus", codecForAny())
|
||||
.property("exchangeStatus", codecForAny())
|
||||
.property("permanent", codecForBoolean())
|
||||
.property("exchangeEntryStatus", codecForAny())
|
||||
.property("exchangeUpdateStatus", codecForAny())
|
||||
.property("ageRestrictionOptions", codecForList(codecForNumber()))
|
||||
.build("ExchangeListItem");
|
||||
|
||||
@ -2651,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;
|
||||
}
|
||||
|
@ -257,8 +257,7 @@ async function createLocalWallet(
|
||||
},
|
||||
cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any,
|
||||
config: {
|
||||
features: {
|
||||
},
|
||||
features: {},
|
||||
testing: {
|
||||
devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"),
|
||||
denomselAllowLate: checkEnvFlag(
|
||||
@ -651,9 +650,12 @@ walletCli
|
||||
});
|
||||
break;
|
||||
case TalerUriAction.Reward: {
|
||||
const res = await wallet.client.call(WalletApiOperation.PrepareReward, {
|
||||
talerRewardUri: uri,
|
||||
});
|
||||
const res = await wallet.client.call(
|
||||
WalletApiOperation.PrepareReward,
|
||||
{
|
||||
talerRewardUri: uri,
|
||||
},
|
||||
);
|
||||
console.log("tip status", res);
|
||||
await wallet.client.call(WalletApiOperation.AcceptReward, {
|
||||
walletRewardId: res.walletRewardId,
|
||||
@ -874,95 +876,32 @@ const backupCli = walletCli.subcommand("backupArgs", "backup", {
|
||||
help: "Subcommands for backups",
|
||||
});
|
||||
|
||||
backupCli
|
||||
.subcommand("setDeviceId", "set-device-id")
|
||||
.requiredArgument("deviceId", clk.STRING, {
|
||||
help: "new device ID",
|
||||
})
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
await wallet.client.call(WalletApiOperation.SetWalletDeviceId, {
|
||||
walletDeviceId: args.setDeviceId.deviceId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
backupCli.subcommand("exportPlain", "export-plain").action(async (args) => {
|
||||
backupCli.subcommand("exportDb", "export-db").action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const backup = await wallet.client.call(
|
||||
WalletApiOperation.ExportBackupPlain,
|
||||
{},
|
||||
);
|
||||
const backup = await wallet.client.call(WalletApiOperation.ExportDb, {});
|
||||
console.log(JSON.stringify(backup, undefined, 2));
|
||||
});
|
||||
});
|
||||
|
||||
backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => {
|
||||
backupCli.subcommand("storeBackup", "store-backup").action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const recoveryJson = await wallet.client.call(
|
||||
WalletApiOperation.ExportBackupRecovery,
|
||||
const resp = await wallet.client.call(
|
||||
WalletApiOperation.CreateStoredBackup,
|
||||
{},
|
||||
);
|
||||
console.log(JSON.stringify(recoveryJson, undefined, 2));
|
||||
console.log(JSON.stringify(resp, undefined, 2));
|
||||
});
|
||||
});
|
||||
|
||||
backupCli.subcommand("run", "run").action(async (args) => {
|
||||
backupCli.subcommand("importDb", "import-db").action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
|
||||
});
|
||||
});
|
||||
|
||||
backupCli.subcommand("status", "status").action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const status = await wallet.client.call(
|
||||
WalletApiOperation.GetBackupInfo,
|
||||
{},
|
||||
);
|
||||
console.log(JSON.stringify(status, undefined, 2));
|
||||
});
|
||||
});
|
||||
|
||||
backupCli
|
||||
.subcommand("recoveryLoad", "load-recovery")
|
||||
.maybeOption("strategy", ["--strategy"], clk.STRING, {
|
||||
help: "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')",
|
||||
})
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const data = JSON.parse(await read(process.stdin));
|
||||
let strategy: RecoveryMergeStrategy | undefined;
|
||||
const stratStr = args.recoveryLoad.strategy;
|
||||
if (stratStr) {
|
||||
if (stratStr === "theirs") {
|
||||
strategy = RecoveryMergeStrategy.Theirs;
|
||||
} else if (stratStr === "ours") {
|
||||
strategy = RecoveryMergeStrategy.Theirs;
|
||||
} else {
|
||||
throw Error("invalid recovery strategy");
|
||||
}
|
||||
}
|
||||
await wallet.client.call(WalletApiOperation.ImportBackupRecovery, {
|
||||
recovery: data,
|
||||
strategy,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
backupCli
|
||||
.subcommand("addProvider", "add-provider")
|
||||
.requiredArgument("url", clk.STRING)
|
||||
.maybeArgument("name", clk.STRING)
|
||||
.flag("activate", ["--activate"])
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
await wallet.client.call(WalletApiOperation.AddBackupProvider, {
|
||||
backupProviderBaseUrl: args.addProvider.url,
|
||||
activate: args.addProvider.activate,
|
||||
name: args.addProvider.name || args.addProvider.url,
|
||||
});
|
||||
const dumpRaw = await read(process.stdin);
|
||||
const dump = JSON.parse(dumpRaw);
|
||||
await wallet.client.call(WalletApiOperation.ImportDb, {
|
||||
dump,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const depositCli = walletCli.subcommand("depositArgs", "deposit", {
|
||||
help: "Subcommands for depositing money to payto:// accounts",
|
||||
@ -1681,6 +1620,3 @@ async function read(stream: NodeJS.ReadStream) {
|
||||
export function main() {
|
||||
walletCli.run();
|
||||
}
|
||||
function classifyTalerUri(uri: string) {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
IDBFactory,
|
||||
IDBObjectStore,
|
||||
IDBTransaction,
|
||||
structuredEncapsulate,
|
||||
} from "@gnu-taler/idb-bridge";
|
||||
import {
|
||||
AgeCommitmentProof,
|
||||
@ -103,7 +104,7 @@ import { RetryInfo, TaskIdentifiers } from "./operations/common.js";
|
||||
* for all previous versions must be written, which should be
|
||||
* avoided.
|
||||
*/
|
||||
export const TALER_DB_NAME = "taler-wallet-main-v9";
|
||||
export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v9";
|
||||
|
||||
/**
|
||||
* Name of the metadata database. This database is used
|
||||
@ -111,7 +112,12 @@ export const TALER_DB_NAME = "taler-wallet-main-v9";
|
||||
*
|
||||
* (Minor migrations are handled via upgrade transactions.)
|
||||
*/
|
||||
export const TALER_META_DB_NAME = "taler-wallet-meta";
|
||||
export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta";
|
||||
|
||||
/**
|
||||
* Stored backups, mainly created when manually importing a backup.
|
||||
*/
|
||||
export const TALER_WALLET_STORED_BACKUPS_DB_NAME = "taler-wallet-stored-backups";
|
||||
|
||||
export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
|
||||
|
||||
@ -566,10 +572,31 @@ export interface ExchangeDetailsPointer {
|
||||
updateClock: TalerPreciseTimestamp;
|
||||
}
|
||||
|
||||
export enum ExchangeEntryDbRecordStatus {
|
||||
Preset = 1,
|
||||
Ephemeral = 2,
|
||||
Used = 3,
|
||||
}
|
||||
|
||||
export enum ExchangeEntryDbUpdateStatus {
|
||||
Initial = 1,
|
||||
InitialUpdate = 2,
|
||||
Suspended = 3,
|
||||
Failed = 4,
|
||||
OutdatedUpdate = 5,
|
||||
Ready = 6,
|
||||
ReadyUpdate = 7,
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp stored as a IEEE 754 double, in milliseconds.
|
||||
*/
|
||||
export type DbIndexableTimestampMs = number;
|
||||
|
||||
/**
|
||||
* Exchange record as stored in the wallet's database.
|
||||
*/
|
||||
export interface ExchangeRecord {
|
||||
export interface ExchangeEntryRecord {
|
||||
/**
|
||||
* Base url of the exchange.
|
||||
*/
|
||||
@ -594,13 +621,12 @@ export interface ExchangeRecord {
|
||||
*/
|
||||
detailsPointer: ExchangeDetailsPointer | undefined;
|
||||
|
||||
/**
|
||||
* Is this a permanent or temporary exchange record?
|
||||
*/
|
||||
permanent: boolean;
|
||||
entryStatus: ExchangeEntryDbRecordStatus;
|
||||
|
||||
updateStatus: ExchangeEntryDbUpdateStatus;
|
||||
|
||||
/**
|
||||
* Last time when the exchange was updated (both /keys and /wire).
|
||||
* Last time when the exchange /keys info was updated.
|
||||
*/
|
||||
lastUpdate: TalerPreciseTimestamp | undefined;
|
||||
|
||||
@ -608,20 +634,21 @@ export interface ExchangeRecord {
|
||||
* Next scheduled update for the exchange.
|
||||
*
|
||||
* (This field must always be present, so we can index on the timestamp.)
|
||||
*
|
||||
* FIXME: To index on the timestamp, this needs to be a number of
|
||||
* binary timestamp!
|
||||
*/
|
||||
nextUpdate: TalerPreciseTimestamp;
|
||||
nextUpdateStampMs: DbIndexableTimestampMs;
|
||||
|
||||
lastKeysEtag: string | undefined;
|
||||
|
||||
lastWireEtag: string | undefined;
|
||||
|
||||
/**
|
||||
* Next time that we should check if coins need to be refreshed.
|
||||
*
|
||||
* Updated whenever the exchange's denominations are updated or when
|
||||
* the refresh check has been done.
|
||||
*/
|
||||
nextRefreshCheck: TalerPreciseTimestamp;
|
||||
nextRefreshCheckStampMs: DbIndexableTimestampMs;
|
||||
|
||||
/**
|
||||
* Public key of the reserve that we're currently using for
|
||||
@ -2431,7 +2458,7 @@ export const WalletStoresV1 = {
|
||||
),
|
||||
exchanges: describeStore(
|
||||
"exchanges",
|
||||
describeContents<ExchangeRecord>({
|
||||
describeContents<ExchangeEntryRecord>({
|
||||
keyPath: "baseUrl",
|
||||
}),
|
||||
{},
|
||||
@ -2725,11 +2752,10 @@ export type WalletDbReadOnlyTransaction<
|
||||
Stores extends StoreNames<typeof WalletStoresV1> & string,
|
||||
> = DbReadOnlyTransaction<typeof WalletStoresV1, Stores>;
|
||||
|
||||
export type WalletReadWriteTransaction<
|
||||
export type WalletDbReadWriteTransaction<
|
||||
Stores extends StoreNames<typeof WalletStoresV1> & string,
|
||||
> = DbReadWriteTransaction<typeof WalletStoresV1, Stores>;
|
||||
|
||||
|
||||
/**
|
||||
* An applied migration.
|
||||
*/
|
||||
@ -2760,45 +2786,144 @@ export const walletMetadataStore = {
|
||||
),
|
||||
};
|
||||
|
||||
export function exportDb(db: IDBDatabase): Promise<any> {
|
||||
const dump = {
|
||||
name: db.name,
|
||||
stores: {} as { [s: string]: any },
|
||||
version: db.version,
|
||||
export interface StoredBackupMeta {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const StoredBackupStores = {
|
||||
backupMeta: describeStore(
|
||||
"backupMeta",
|
||||
describeContents<StoredBackupMeta>({ keyPath: "name" }),
|
||||
{},
|
||||
),
|
||||
backupData: describeStore("backupData", describeContents<any>({}), {}),
|
||||
};
|
||||
|
||||
export interface DbDumpRecord {
|
||||
/**
|
||||
* Key, serialized with structuredEncapsulated.
|
||||
*
|
||||
* Only present for out-of-line keys (i.e. no key path).
|
||||
*/
|
||||
key?: any;
|
||||
/**
|
||||
* Value, serialized with structuredEncapsulated.
|
||||
*/
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface DbIndexDump {
|
||||
keyPath: string | string[];
|
||||
multiEntry: boolean;
|
||||
unique: boolean;
|
||||
}
|
||||
|
||||
export interface DbStoreDump {
|
||||
keyPath?: string | string[];
|
||||
autoIncrement: boolean;
|
||||
indexes: { [indexName: string]: DbIndexDump };
|
||||
records: DbDumpRecord[];
|
||||
}
|
||||
|
||||
export interface DbDumpDatabase {
|
||||
version: number;
|
||||
stores: { [storeName: string]: DbStoreDump };
|
||||
}
|
||||
|
||||
export interface DbDump {
|
||||
databases: {
|
||||
[name: string]: DbDumpDatabase;
|
||||
};
|
||||
}
|
||||
|
||||
export async function exportSingleDb(
|
||||
idb: IDBFactory,
|
||||
dbName: string,
|
||||
): Promise<DbDumpDatabase> {
|
||||
const myDb = await openDatabase(
|
||||
idb,
|
||||
dbName,
|
||||
undefined,
|
||||
() => {
|
||||
// May not happen, since we're not requesting a specific version
|
||||
throw Error("unexpected version change");
|
||||
},
|
||||
() => {
|
||||
logger.info("unexpected onupgradeneeded");
|
||||
},
|
||||
);
|
||||
|
||||
const singleDbDump: DbDumpDatabase = {
|
||||
version: myDb.version,
|
||||
stores: {},
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(Array.from(db.objectStoreNames));
|
||||
const tx = myDb.transaction(Array.from(myDb.objectStoreNames));
|
||||
tx.addEventListener("complete", () => {
|
||||
resolve(dump);
|
||||
myDb.close();
|
||||
resolve(singleDbDump);
|
||||
});
|
||||
// tslint:disable-next-line:prefer-for-of
|
||||
for (let i = 0; i < db.objectStoreNames.length; i++) {
|
||||
const name = db.objectStoreNames[i];
|
||||
const storeDump = {} as { [s: string]: any };
|
||||
dump.stores[name] = storeDump;
|
||||
tx.objectStore(name)
|
||||
.openCursor()
|
||||
.addEventListener("success", (e: Event) => {
|
||||
const cursor = (e.target as any).result;
|
||||
if (cursor) {
|
||||
storeDump[cursor.key] = cursor.value;
|
||||
cursor.continue();
|
||||
for (let i = 0; i < myDb.objectStoreNames.length; i++) {
|
||||
const name = myDb.objectStoreNames[i];
|
||||
const store = tx.objectStore(name);
|
||||
const storeDump: DbStoreDump = {
|
||||
autoIncrement: store.autoIncrement,
|
||||
keyPath: store.keyPath,
|
||||
indexes: {},
|
||||
records: [],
|
||||
};
|
||||
const indexNames = store.indexNames;
|
||||
for (let j = 0; j < indexNames.length; j++) {
|
||||
const idxName = indexNames[j];
|
||||
const index = store.index(idxName);
|
||||
storeDump.indexes[idxName] = {
|
||||
keyPath: index.keyPath,
|
||||
multiEntry: index.multiEntry,
|
||||
unique: index.unique,
|
||||
};
|
||||
}
|
||||
singleDbDump.stores[name] = storeDump;
|
||||
store.openCursor().addEventListener("success", (e: Event) => {
|
||||
const cursor = (e.target as any).result;
|
||||
if (cursor) {
|
||||
const rec: DbDumpRecord = {
|
||||
value: structuredEncapsulate(cursor.value),
|
||||
};
|
||||
// Only store key if necessary, i.e. when
|
||||
// the key is not stored as part of the object via
|
||||
// a key path.
|
||||
if (store.keyPath == null) {
|
||||
rec.key = structuredEncapsulate(cursor.key);
|
||||
}
|
||||
});
|
||||
cursor.continue();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export interface DatabaseDump {
|
||||
name: string;
|
||||
stores: { [s: string]: any };
|
||||
version: string;
|
||||
export async function exportDb(idb: IDBFactory): Promise<DbDump> {
|
||||
const dbDump: DbDump = {
|
||||
databases: {},
|
||||
};
|
||||
|
||||
dbDump.databases[TALER_WALLET_META_DB_NAME] = await exportSingleDb(
|
||||
idb,
|
||||
TALER_WALLET_META_DB_NAME,
|
||||
);
|
||||
dbDump.databases[TALER_WALLET_MAIN_DB_NAME] = await exportSingleDb(
|
||||
idb,
|
||||
TALER_WALLET_MAIN_DB_NAME,
|
||||
);
|
||||
|
||||
return dbDump;
|
||||
}
|
||||
|
||||
async function recoverFromDump(
|
||||
db: IDBDatabase,
|
||||
dump: DatabaseDump,
|
||||
dbDump: DbDumpDatabase,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
|
||||
@ -2807,67 +2932,33 @@ async function recoverFromDump(
|
||||
});
|
||||
for (let i = 0; i < db.objectStoreNames.length; i++) {
|
||||
const name = db.objectStoreNames[i];
|
||||
const storeDump = dump.stores[name];
|
||||
const storeDump = dbDump.stores[name];
|
||||
if (!storeDump) continue;
|
||||
Object.keys(storeDump).forEach(async (key) => {
|
||||
const value = storeDump[key];
|
||||
if (!value) return;
|
||||
tx.objectStore(name).put(value);
|
||||
});
|
||||
for (let rec of storeDump.records) {
|
||||
tx.objectStore(name).put(rec.value, rec.key);
|
||||
}
|
||||
}
|
||||
tx.commit();
|
||||
});
|
||||
}
|
||||
|
||||
export async function importDb(db: IDBDatabase, object: any): Promise<void> {
|
||||
if ("name" in object && "stores" in object && "version" in object) {
|
||||
// looks like a database dump
|
||||
const dump = object as DatabaseDump;
|
||||
return recoverFromDump(db, dump);
|
||||
}
|
||||
function checkDbDump(x: any): x is DbDump {
|
||||
return "databases" in x;
|
||||
}
|
||||
|
||||
if ("databases" in object && "$types" in object) {
|
||||
// looks like a IDBDatabase
|
||||
const someDatabase = object.databases;
|
||||
|
||||
if (TALER_META_DB_NAME in someDatabase) {
|
||||
//looks like a taler database
|
||||
const currentMainDbValue =
|
||||
someDatabase[TALER_META_DB_NAME].objectStores.metaConfig.records[0]
|
||||
.value.value;
|
||||
|
||||
if (currentMainDbValue !== TALER_DB_NAME) {
|
||||
console.log("not the current database version");
|
||||
}
|
||||
|
||||
const talerDb = someDatabase[currentMainDbValue];
|
||||
|
||||
const objectStoreNames = Object.keys(talerDb.objectStores);
|
||||
|
||||
const dump: DatabaseDump = {
|
||||
name: talerDb.schema.databaseName,
|
||||
version: talerDb.schema.databaseVersion,
|
||||
stores: {},
|
||||
};
|
||||
|
||||
for (let i = 0; i < objectStoreNames.length; i++) {
|
||||
const name = objectStoreNames[i];
|
||||
const storeDump = {} as { [s: string]: any };
|
||||
dump.stores[name] = storeDump;
|
||||
talerDb.objectStores[name].records.map((r: any) => {
|
||||
const pkey = r.primaryKey;
|
||||
const key =
|
||||
typeof pkey === "string" || typeof pkey === "number"
|
||||
? pkey
|
||||
: pkey.join(",");
|
||||
storeDump[key] = r.value;
|
||||
});
|
||||
}
|
||||
|
||||
return recoverFromDump(db, dump);
|
||||
export async function importDb(db: IDBDatabase, dumpJson: any): Promise<void> {
|
||||
const d = dumpJson;
|
||||
if (checkDbDump(d)) {
|
||||
const walletDb = d.databases[TALER_WALLET_MAIN_DB_NAME];
|
||||
if (!walletDb) {
|
||||
throw Error(
|
||||
`unable to import, main wallet database (${TALER_WALLET_MAIN_DB_NAME}) not found`,
|
||||
);
|
||||
}
|
||||
await recoverFromDump(db, walletDb);
|
||||
} else {
|
||||
throw Error("unable to import, doesn't look like a valid DB dump");
|
||||
}
|
||||
throw Error("could not import database");
|
||||
}
|
||||
|
||||
export interface FixupDescription {
|
||||
@ -3151,6 +3242,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_STORED_BACKUPS_DB_NAME,
|
||||
1,
|
||||
() => {},
|
||||
onStoredBackupsDbUpgradeNeeded,
|
||||
);
|
||||
|
||||
const handle = new DbAccess(backupsDbHandle, StoredBackupStores);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that resolves
|
||||
* to the taler wallet db.
|
||||
@ -3164,7 +3285,7 @@ export async function openTalerDatabase(
|
||||
): Promise<DbAccess<typeof WalletStoresV1>> {
|
||||
const metaDbHandle = await openDatabase(
|
||||
idbFactory,
|
||||
TALER_META_DB_NAME,
|
||||
TALER_WALLET_META_DB_NAME,
|
||||
1,
|
||||
() => {},
|
||||
onMetaDbUpgradeNeeded,
|
||||
@ -3177,17 +3298,17 @@ export async function openTalerDatabase(
|
||||
.runReadWrite(async (tx) => {
|
||||
const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY);
|
||||
if (!dbVersionRecord) {
|
||||
currentMainVersion = TALER_DB_NAME;
|
||||
currentMainVersion = TALER_WALLET_MAIN_DB_NAME;
|
||||
await tx.metaConfig.put({
|
||||
key: CURRENT_DB_CONFIG_KEY,
|
||||
value: TALER_DB_NAME,
|
||||
value: TALER_WALLET_MAIN_DB_NAME,
|
||||
});
|
||||
} else {
|
||||
currentMainVersion = dbVersionRecord.value;
|
||||
}
|
||||
});
|
||||
|
||||
if (currentMainVersion !== TALER_DB_NAME) {
|
||||
if (currentMainVersion !== TALER_WALLET_MAIN_DB_NAME) {
|
||||
switch (currentMainVersion) {
|
||||
case "taler-wallet-main-v2":
|
||||
case "taler-wallet-main-v3":
|
||||
@ -3203,7 +3324,7 @@ export async function openTalerDatabase(
|
||||
.runReadWrite(async (tx) => {
|
||||
await tx.metaConfig.put({
|
||||
key: CURRENT_DB_CONFIG_KEY,
|
||||
value: TALER_DB_NAME,
|
||||
value: TALER_WALLET_MAIN_DB_NAME,
|
||||
});
|
||||
});
|
||||
break;
|
||||
@ -3216,7 +3337,7 @@ export async function openTalerDatabase(
|
||||
|
||||
const mainDbHandle = await openDatabase(
|
||||
idbFactory,
|
||||
TALER_DB_NAME,
|
||||
TALER_WALLET_MAIN_DB_NAME,
|
||||
WALLET_DB_MINOR_VERSION,
|
||||
onVersionChange,
|
||||
onTalerDbUpgradeNeeded,
|
||||
@ -3233,7 +3354,7 @@ export async function deleteTalerDatabase(
|
||||
idbFactory: IDBFactory,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = idbFactory.deleteDatabase(TALER_DB_NAME);
|
||||
const req = idbFactory.deleteDatabase(TALER_WALLET_MAIN_DB_NAME);
|
||||
req.onerror = () => reject(req.error);
|
||||
req.onsuccess = () => resolve();
|
||||
});
|
||||
|
@ -139,13 +139,6 @@ export async function createNativeWalletHost2(
|
||||
});
|
||||
}
|
||||
|
||||
const myVersionChange = (): Promise<void> => {
|
||||
logger.error("version change requested, should not happen");
|
||||
throw Error(
|
||||
"BUG: wallet DB version change event can't happen with memory IDB",
|
||||
);
|
||||
};
|
||||
|
||||
let dbResp: MakeDbResult;
|
||||
|
||||
if (args.persistentStoragePath &&args.persistentStoragePath.endsWith(".json")) {
|
||||
@ -160,8 +153,6 @@ export async function createNativeWalletHost2(
|
||||
|
||||
shimIndexedDB(dbResp.idbFactory);
|
||||
|
||||
const myDb = await openTalerDatabase(myIdbFactory, myVersionChange);
|
||||
|
||||
let workerFactory;
|
||||
const cryptoWorkerType = args.cryptoWorkerType ?? "node-worker-thread";
|
||||
if (cryptoWorkerType === "sync") {
|
||||
@ -189,7 +180,7 @@ export async function createNativeWalletHost2(
|
||||
const timer = new SetTimeoutTimerAPI();
|
||||
|
||||
const w = await Wallet.create(
|
||||
myDb,
|
||||
myIdbFactory,
|
||||
myHttpLib,
|
||||
timer,
|
||||
workerFactory,
|
||||
|
@ -110,7 +110,7 @@ async function makeSqliteDb(
|
||||
return {
|
||||
...myBackend.accessStats,
|
||||
primitiveStatements: numStmt,
|
||||
}
|
||||
};
|
||||
},
|
||||
idbFactory: myBridgeIdbFactory,
|
||||
};
|
||||
@ -167,12 +167,15 @@ export async function createNativeWalletHost2(
|
||||
|
||||
let dbResp: MakeDbResult;
|
||||
|
||||
if (args.persistentStoragePath && args.persistentStoragePath.endsWith(".json")) {
|
||||
if (
|
||||
args.persistentStoragePath &&
|
||||
args.persistentStoragePath.endsWith(".json")
|
||||
) {
|
||||
logger.info("using JSON file DB backend (slow!)");
|
||||
dbResp = await makeFileDb(args);
|
||||
} else {
|
||||
logger.info("using sqlite3 DB backend (experimental!)");
|
||||
dbResp = await makeSqliteDb(args)
|
||||
dbResp = await makeSqliteDb(args);
|
||||
}
|
||||
|
||||
const myIdbFactory: IDBFactory = dbResp.idbFactory as any as IDBFactory;
|
||||
@ -189,22 +192,13 @@ export async function createNativeWalletHost2(
|
||||
});
|
||||
}
|
||||
|
||||
const myVersionChange = (): Promise<void> => {
|
||||
logger.error("version change requested, should not happen");
|
||||
throw Error(
|
||||
"BUG: wallet DB version change event can't happen with memory IDB",
|
||||
);
|
||||
};
|
||||
|
||||
const myDb = await openTalerDatabase(myIdbFactory, myVersionChange);
|
||||
|
||||
let workerFactory;
|
||||
workerFactory = new SynchronousCryptoWorkerFactoryPlain();
|
||||
|
||||
const timer = new SetTimeoutTimerAPI();
|
||||
|
||||
const w = await Wallet.create(
|
||||
myDb,
|
||||
myIdbFactory,
|
||||
myHttpLib,
|
||||
timer,
|
||||
workerFactory,
|
||||
|
@ -42,7 +42,7 @@ import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
|
||||
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
||||
import {
|
||||
ExchangeDetailsRecord,
|
||||
ExchangeRecord,
|
||||
ExchangeEntryRecord,
|
||||
RefreshReasonDetails,
|
||||
WalletStoresV1,
|
||||
} from "./db.js";
|
||||
@ -54,6 +54,7 @@ import {
|
||||
} from "./util/query.js";
|
||||
import { TimerGroup } from "./util/timer.js";
|
||||
import { WalletConfig } from "./wallet-api-types.js";
|
||||
import { IDBFactory } from "@gnu-taler/idb-bridge";
|
||||
|
||||
export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
|
||||
export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
|
||||
@ -108,7 +109,7 @@ export interface ExchangeOperations {
|
||||
): Promise<ExchangeDetailsRecord | undefined>;
|
||||
getExchangeTrust(
|
||||
ws: InternalWalletState,
|
||||
exchangeInfo: ExchangeRecord,
|
||||
exchangeInfo: ExchangeEntryRecord,
|
||||
): Promise<TrustInfo>;
|
||||
updateExchangeFromUrl(
|
||||
ws: InternalWalletState,
|
||||
@ -118,7 +119,7 @@ export interface ExchangeOperations {
|
||||
cancellationToken?: CancellationToken;
|
||||
},
|
||||
): Promise<{
|
||||
exchange: ExchangeRecord;
|
||||
exchange: ExchangeEntryRecord;
|
||||
exchangeDetails: ExchangeDetailsRecord;
|
||||
}>;
|
||||
}
|
||||
@ -203,6 +204,9 @@ export interface InternalWalletState {
|
||||
denomPubHash: string,
|
||||
): Promise<DenominationInfo | undefined>;
|
||||
|
||||
ensureWalletDbOpen(): Promise<void>;
|
||||
|
||||
idb: IDBFactory;
|
||||
db: DbAccess<typeof WalletStoresV1>;
|
||||
http: HttpRequestLibrary;
|
||||
|
||||
|
@ -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,875 +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,
|
||||
},
|
||||
permanent: true,
|
||||
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;
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import {
|
||||
ExchangeEntryStatus,
|
||||
ExchangeListItem,
|
||||
ExchangeTosStatus,
|
||||
ExchangeUpdateStatus,
|
||||
getErrorDetailFromException,
|
||||
j2s,
|
||||
Logger,
|
||||
@ -47,7 +48,7 @@ import {
|
||||
WalletStoresV1,
|
||||
CoinRecord,
|
||||
ExchangeDetailsRecord,
|
||||
ExchangeRecord,
|
||||
ExchangeEntryRecord,
|
||||
BackupProviderRecord,
|
||||
DepositGroupRecord,
|
||||
PeerPullPaymentIncomingRecord,
|
||||
@ -59,6 +60,8 @@ import {
|
||||
RefreshGroupRecord,
|
||||
RewardRecord,
|
||||
WithdrawalGroupRecord,
|
||||
ExchangeEntryDbUpdateStatus,
|
||||
ExchangeEntryDbRecordStatus,
|
||||
} from "../db.js";
|
||||
import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";
|
||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||
@ -529,16 +532,16 @@ export function getExchangeTosStatus(
|
||||
exchangeDetails: ExchangeDetailsRecord,
|
||||
): ExchangeTosStatus {
|
||||
if (!exchangeDetails.tosAccepted) {
|
||||
return ExchangeTosStatus.New;
|
||||
return ExchangeTosStatus.Proposed;
|
||||
}
|
||||
if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {
|
||||
return ExchangeTosStatus.Accepted;
|
||||
}
|
||||
return ExchangeTosStatus.Changed;
|
||||
return ExchangeTosStatus.Proposed;
|
||||
}
|
||||
|
||||
export function makeExchangeListItem(
|
||||
r: ExchangeRecord,
|
||||
r: ExchangeEntryRecord,
|
||||
exchangeDetails: ExchangeDetailsRecord | undefined,
|
||||
lastError: TalerErrorDetail | undefined,
|
||||
): ExchangeListItem {
|
||||
@ -547,30 +550,57 @@ export function makeExchangeListItem(
|
||||
error: lastError,
|
||||
}
|
||||
: undefined;
|
||||
if (!exchangeDetails) {
|
||||
return {
|
||||
exchangeBaseUrl: r.baseUrl,
|
||||
currency: undefined,
|
||||
tosStatus: ExchangeTosStatus.Unknown,
|
||||
paytoUris: [],
|
||||
exchangeStatus: ExchangeEntryStatus.Unknown,
|
||||
permanent: r.permanent,
|
||||
ageRestrictionOptions: [],
|
||||
lastUpdateErrorInfo,
|
||||
};
|
||||
|
||||
let exchangeUpdateStatus: ExchangeUpdateStatus;
|
||||
switch (r.updateStatus) {
|
||||
case ExchangeEntryDbUpdateStatus.Failed:
|
||||
exchangeUpdateStatus = ExchangeUpdateStatus.Failed;
|
||||
break;
|
||||
case ExchangeEntryDbUpdateStatus.Initial:
|
||||
exchangeUpdateStatus = ExchangeUpdateStatus.Initial;
|
||||
break;
|
||||
case ExchangeEntryDbUpdateStatus.InitialUpdate:
|
||||
exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate;
|
||||
break;
|
||||
case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
|
||||
exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate;
|
||||
break;
|
||||
case ExchangeEntryDbUpdateStatus.Ready:
|
||||
exchangeUpdateStatus = ExchangeUpdateStatus.Ready;
|
||||
break;
|
||||
case ExchangeEntryDbUpdateStatus.ReadyUpdate:
|
||||
exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate;
|
||||
break;
|
||||
case ExchangeEntryDbUpdateStatus.Suspended:
|
||||
exchangeUpdateStatus = ExchangeUpdateStatus.Suspended;
|
||||
break;
|
||||
}
|
||||
let exchangeStatus;
|
||||
exchangeStatus = ExchangeEntryStatus.Ok;
|
||||
|
||||
let exchangeEntryStatus: ExchangeEntryStatus;
|
||||
switch (r.entryStatus) {
|
||||
case ExchangeEntryDbRecordStatus.Ephemeral:
|
||||
exchangeEntryStatus = ExchangeEntryStatus.Ephemeral;
|
||||
break;
|
||||
case ExchangeEntryDbRecordStatus.Preset:
|
||||
exchangeEntryStatus = ExchangeEntryStatus.Preset;
|
||||
break;
|
||||
case ExchangeEntryDbRecordStatus.Used:
|
||||
exchangeEntryStatus = ExchangeEntryStatus.Used;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
exchangeBaseUrl: r.baseUrl,
|
||||
currency: exchangeDetails.currency,
|
||||
tosStatus: getExchangeTosStatus(exchangeDetails),
|
||||
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
|
||||
exchangeStatus,
|
||||
permanent: r.permanent,
|
||||
ageRestrictionOptions: exchangeDetails.ageMask
|
||||
currency: exchangeDetails?.currency,
|
||||
exchangeUpdateStatus,
|
||||
exchangeEntryStatus,
|
||||
tosStatus: exchangeDetails
|
||||
? getExchangeTosStatus(exchangeDetails)
|
||||
: ExchangeTosStatus.Pending,
|
||||
ageRestrictionOptions: exchangeDetails?.ageMask
|
||||
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
|
||||
: [],
|
||||
paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [],
|
||||
lastUpdateErrorInfo,
|
||||
};
|
||||
}
|
||||
@ -892,13 +922,13 @@ export namespace TaskIdentifiers {
|
||||
export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId {
|
||||
return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
|
||||
}
|
||||
export function forExchangeUpdate(exch: ExchangeRecord): TaskId {
|
||||
export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskId {
|
||||
return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
|
||||
}
|
||||
export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
|
||||
return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
|
||||
}
|
||||
export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
|
||||
export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId {
|
||||
return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
|
||||
}
|
||||
export function forTipPickup(tipRecord: RewardRecord): TaskId {
|
||||
|
@ -32,6 +32,7 @@ import {
|
||||
encodeCrock,
|
||||
ExchangeAuditor,
|
||||
ExchangeDenomination,
|
||||
ExchangeEntryStatus,
|
||||
ExchangeGlobalFees,
|
||||
ExchangeSignKeyJson,
|
||||
ExchangeWireJson,
|
||||
@ -66,10 +67,15 @@ import {
|
||||
DenominationRecord,
|
||||
DenominationVerificationStatus,
|
||||
ExchangeDetailsRecord,
|
||||
ExchangeRecord,
|
||||
ExchangeEntryRecord,
|
||||
WalletStoresV1,
|
||||
} from "../db.js";
|
||||
import { isWithdrawableDenom } from "../index.js";
|
||||
import {
|
||||
ExchangeEntryDbRecordStatus,
|
||||
ExchangeEntryDbUpdateStatus,
|
||||
isWithdrawableDenom,
|
||||
WalletDbReadWriteTransaction,
|
||||
} from "../index.js";
|
||||
import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";
|
||||
import { checkDbInvariant } from "../util/invariants.js";
|
||||
import {
|
||||
@ -326,6 +332,26 @@ export async function downloadExchangeInfo(
|
||||
};
|
||||
}
|
||||
|
||||
export async function addPresetExchangeEntry(
|
||||
tx: WalletDbReadWriteTransaction<"exchanges">,
|
||||
exchangeBaseUrl: string,
|
||||
): Promise<void> {
|
||||
let exchange = await tx.exchanges.get(exchangeBaseUrl);
|
||||
if (!exchange) {
|
||||
const r: ExchangeEntryRecord = {
|
||||
entryStatus: ExchangeEntryDbRecordStatus.Preset,
|
||||
updateStatus: ExchangeEntryDbUpdateStatus.Initial,
|
||||
baseUrl: exchangeBaseUrl,
|
||||
detailsPointer: undefined,
|
||||
lastUpdate: undefined,
|
||||
lastKeysEtag: undefined,
|
||||
nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(),
|
||||
nextUpdateStampMs: AbsoluteTime.getStampMsNever(),
|
||||
};
|
||||
await tx.exchanges.put(r);
|
||||
}
|
||||
}
|
||||
|
||||
export async function provideExchangeRecordInTx(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadWriteAccess<{
|
||||
@ -335,20 +361,20 @@ export async function provideExchangeRecordInTx(
|
||||
baseUrl: string,
|
||||
now: AbsoluteTime,
|
||||
): Promise<{
|
||||
exchange: ExchangeRecord;
|
||||
exchange: ExchangeEntryRecord;
|
||||
exchangeDetails: ExchangeDetailsRecord | undefined;
|
||||
}> {
|
||||
let exchange = await tx.exchanges.get(baseUrl);
|
||||
if (!exchange) {
|
||||
const r: ExchangeRecord = {
|
||||
permanent: true,
|
||||
const r: ExchangeEntryRecord = {
|
||||
entryStatus: ExchangeEntryDbRecordStatus.Ephemeral,
|
||||
updateStatus: ExchangeEntryDbUpdateStatus.InitialUpdate,
|
||||
baseUrl: baseUrl,
|
||||
detailsPointer: undefined,
|
||||
lastUpdate: undefined,
|
||||
nextUpdate: AbsoluteTime.toPreciseTimestamp(now),
|
||||
nextRefreshCheck: AbsoluteTime.toPreciseTimestamp(now),
|
||||
nextUpdateStampMs: AbsoluteTime.getStampMsNever(),
|
||||
nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(),
|
||||
lastKeysEtag: undefined,
|
||||
lastWireEtag: undefined,
|
||||
};
|
||||
await tx.exchanges.put(r);
|
||||
exchange = r;
|
||||
@ -534,6 +560,10 @@ export async function downloadTosFromAcceptedFormat(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: Split this into two parts: (a) triggering the exchange
|
||||
* to be updated and (b) waiting for the update to finish.
|
||||
*/
|
||||
export async function updateExchangeFromUrl(
|
||||
ws: InternalWalletState,
|
||||
baseUrl: string,
|
||||
@ -543,7 +573,7 @@ export async function updateExchangeFromUrl(
|
||||
cancellationToken?: CancellationToken;
|
||||
} = {},
|
||||
): Promise<{
|
||||
exchange: ExchangeRecord;
|
||||
exchange: ExchangeEntryRecord;
|
||||
exchangeDetails: ExchangeDetailsRecord;
|
||||
}> {
|
||||
const canonUrl = canonicalizeBaseUrl(baseUrl);
|
||||
@ -613,7 +643,7 @@ export async function updateExchangeFromUrlHandler(
|
||||
!forceNow &&
|
||||
exchangeDetails !== undefined &&
|
||||
!AbsoluteTime.isExpired(
|
||||
AbsoluteTime.fromPreciseTimestamp(exchange.nextUpdate),
|
||||
AbsoluteTime.fromStampMs(exchange.nextUpdateStampMs),
|
||||
)
|
||||
) {
|
||||
logger.trace("using existing exchange info");
|
||||
@ -755,11 +785,11 @@ export async function updateExchangeFromUrlHandler(
|
||||
newDetails.rowId = existingDetails.rowId;
|
||||
}
|
||||
r.lastUpdate = TalerPreciseTimestamp.now();
|
||||
r.nextUpdate = AbsoluteTime.toPreciseTimestamp(
|
||||
r.nextUpdateStampMs = AbsoluteTime.toStampMs(
|
||||
AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry),
|
||||
);
|
||||
// New denominations might be available.
|
||||
r.nextRefreshCheck = TalerPreciseTimestamp.now();
|
||||
r.nextRefreshCheckStampMs = AbsoluteTime.getStampMsNow();
|
||||
if (detailsPointerChanged) {
|
||||
r.detailsPointer = {
|
||||
currency: newDetails.currency,
|
||||
@ -948,7 +978,7 @@ export async function getExchangePaytoUri(
|
||||
*/
|
||||
export async function getExchangeTrust(
|
||||
ws: InternalWalletState,
|
||||
exchangeInfo: ExchangeRecord,
|
||||
exchangeInfo: ExchangeEntryRecord,
|
||||
): Promise<TrustInfo> {
|
||||
let isTrusted = false;
|
||||
let isAudited = false;
|
||||
|
@ -45,6 +45,7 @@ import {
|
||||
PeerPushPaymentIncomingRecord,
|
||||
RefundGroupRecord,
|
||||
RefundGroupStatus,
|
||||
ExchangeEntryDbUpdateStatus,
|
||||
} from "../db.js";
|
||||
import {
|
||||
PendingOperationsResponse,
|
||||
@ -81,19 +82,25 @@ async function gatherExchangePending(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadOnlyAccess<{
|
||||
exchanges: typeof WalletStoresV1.exchanges;
|
||||
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
||||
operationRetries: typeof WalletStoresV1.operationRetries;
|
||||
}>,
|
||||
now: AbsoluteTime,
|
||||
resp: PendingOperationsResponse,
|
||||
): Promise<void> {
|
||||
// FIXME: We should do a range query here based on the update time.
|
||||
// FIXME: We should do a range query here based on the update time
|
||||
// and/or the entry state.
|
||||
await tx.exchanges.iter().forEachAsync(async (exch) => {
|
||||
switch (exch.updateStatus) {
|
||||
case ExchangeEntryDbUpdateStatus.Initial:
|
||||
case ExchangeEntryDbUpdateStatus.Suspended:
|
||||
case ExchangeEntryDbUpdateStatus.Failed:
|
||||
return;
|
||||
}
|
||||
const opTag = TaskIdentifiers.forExchangeUpdate(exch);
|
||||
let opr = await tx.operationRetries.get(opTag);
|
||||
const timestampDue =
|
||||
opr?.retryInfo.nextRetry ??
|
||||
AbsoluteTime.fromPreciseTimestamp(exch.nextUpdate);
|
||||
AbsoluteTime.fromStampMs(exch.nextUpdateStampMs);
|
||||
resp.pendingOperations.push({
|
||||
type: PendingTaskType.ExchangeUpdate,
|
||||
...getPendingCommon(ws, opTag, timestampDue),
|
||||
@ -108,7 +115,7 @@ async function gatherExchangePending(
|
||||
resp.pendingOperations.push({
|
||||
type: PendingTaskType.ExchangeCheckRefresh,
|
||||
...getPendingCommon(ws, opTag, timestampDue),
|
||||
timestampDue: AbsoluteTime.fromPreciseTimestamp(exch.nextRefreshCheck),
|
||||
timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs),
|
||||
givesLifeness: false,
|
||||
exchangeBaseUrl: exch.baseUrl,
|
||||
});
|
||||
@ -184,8 +191,9 @@ export async function iterRecordsForWithdrawal(
|
||||
WithdrawalGroupStatus.PendingRegisteringBank,
|
||||
WithdrawalGroupStatus.PendingAml,
|
||||
);
|
||||
withdrawalGroupRecords =
|
||||
await tx.withdrawalGroups.indexes.byStatus.getAll(range);
|
||||
withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll(
|
||||
range,
|
||||
);
|
||||
} else {
|
||||
withdrawalGroupRecords =
|
||||
await tx.withdrawalGroups.indexes.byStatus.getAll();
|
||||
@ -344,12 +352,8 @@ export async function iterRecordsForRefund(
|
||||
f: (r: RefundGroupRecord) => Promise<void>,
|
||||
): Promise<void> {
|
||||
if (filter.onlyState === "nonfinal") {
|
||||
const keyRange = GlobalIDB.KeyRange.only(
|
||||
RefundGroupStatus.Pending
|
||||
);
|
||||
await tx.refundGroups.indexes.byStatus
|
||||
.iter(keyRange)
|
||||
.forEachAsync(f);
|
||||
const keyRange = GlobalIDB.KeyRange.only(RefundGroupStatus.Pending);
|
||||
await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);
|
||||
} else {
|
||||
await tx.refundGroups.iter().forEachAsync(f);
|
||||
}
|
||||
|
@ -1190,14 +1190,14 @@ export async function autoRefresh(
|
||||
`created refresh group for auto-refresh (${res.refreshGroupId})`,
|
||||
);
|
||||
}
|
||||
// logger.trace(
|
||||
// `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`,
|
||||
// );
|
||||
// logger.trace(
|
||||
// `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`,
|
||||
// );
|
||||
logger.trace(
|
||||
`next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,
|
||||
);
|
||||
exchange.nextRefreshCheck =
|
||||
AbsoluteTime.toPreciseTimestamp(minCheckThreshold);
|
||||
exchange.nextRefreshCheckStampMs =
|
||||
AbsoluteTime.toStampMs(minCheckThreshold);
|
||||
await tx.exchanges.put(exchange);
|
||||
});
|
||||
return TaskRunResult.finished();
|
||||
|
@ -150,14 +150,14 @@ export async function prepareTip(
|
||||
.mktx((x) => [x.rewards])
|
||||
.runReadOnly(async (tx) => {
|
||||
return tx.rewards.indexes.byMerchantTipIdAndBaseUrl.get([
|
||||
res.merchantTipId,
|
||||
res.merchantRewardId,
|
||||
res.merchantBaseUrl,
|
||||
]);
|
||||
});
|
||||
|
||||
if (!tipRecord) {
|
||||
const tipStatusUrl = new URL(
|
||||
`tips/${res.merchantTipId}`,
|
||||
`rewards/${res.merchantRewardId}`,
|
||||
res.merchantBaseUrl,
|
||||
);
|
||||
logger.trace("checking tip status from", tipStatusUrl.href);
|
||||
@ -204,7 +204,7 @@ export async function prepareTip(
|
||||
next_url: tipPickupStatus.next_url,
|
||||
merchantBaseUrl: res.merchantBaseUrl,
|
||||
createdTimestamp: TalerPreciseTimestamp.now(),
|
||||
merchantRewardId: res.merchantTipId,
|
||||
merchantRewardId: res.merchantRewardId,
|
||||
rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
|
||||
denomsSel: selectedDenoms,
|
||||
pickedUpTimestamp: undefined,
|
||||
|
@ -132,6 +132,8 @@ import {
|
||||
} from "../util/coinSelection.js";
|
||||
import {
|
||||
ExchangeDetailsRecord,
|
||||
ExchangeEntryDbRecordStatus,
|
||||
ExchangeEntryDbUpdateStatus,
|
||||
PendingTaskType,
|
||||
isWithdrawableDenom,
|
||||
} from "../index.js";
|
||||
@ -2346,10 +2348,6 @@ export async function internalPerformCreateWithdrawalGroup(
|
||||
}>,
|
||||
prep: PrepareCreateWithdrawalGroupResult,
|
||||
): Promise<PerformCreateWithdrawalGroupResult> {
|
||||
const transactionId = constructTransactionIdentifier({
|
||||
tag: TransactionType.Withdrawal,
|
||||
withdrawalGroupId: prep.withdrawalGroup.withdrawalGroupId,
|
||||
});
|
||||
const { withdrawalGroup } = prep;
|
||||
if (!prep.creationInfo) {
|
||||
return { withdrawalGroup, transitionInfo: undefined };
|
||||
@ -2366,6 +2364,7 @@ export async function internalPerformCreateWithdrawalGroup(
|
||||
const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
|
||||
if (exchange) {
|
||||
exchange.lastWithdrawal = TalerPreciseTimestamp.now();
|
||||
exchange.entryStatus = ExchangeEntryDbRecordStatus.Used;
|
||||
await tx.exchanges.put(exchange);
|
||||
}
|
||||
|
||||
|
@ -239,7 +239,7 @@ class ResultStream<T> {
|
||||
export function openDatabase(
|
||||
idbFactory: IDBFactory,
|
||||
databaseName: string,
|
||||
databaseVersion: number,
|
||||
databaseVersion: number | undefined,
|
||||
onVersionChange: () => void,
|
||||
onUpgradeNeeded: (
|
||||
db: IDBDatabase,
|
||||
@ -257,7 +257,7 @@ export function openDatabase(
|
||||
req.onsuccess = (e) => {
|
||||
req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
|
||||
logger.info(
|
||||
`handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`,
|
||||
`handling versionchange on ${databaseName} from ${evt.oldVersion} to ${evt.newVersion}`,
|
||||
);
|
||||
req.result.close();
|
||||
onVersionChange();
|
||||
@ -274,6 +274,9 @@ export function openDatabase(
|
||||
if (!transaction) {
|
||||
throw Error("no transaction handle available in upgrade handler");
|
||||
}
|
||||
logger.info(
|
||||
`handling upgradeneeded event on ${databaseName} from ${e.oldVersion} to ${e.newVersion}`,
|
||||
);
|
||||
onUpgradeNeeded(db, e.oldVersion, newVersion, transaction);
|
||||
};
|
||||
});
|
||||
@ -376,8 +379,8 @@ export interface InsertResponse {
|
||||
export interface StoreReadWriteAccessor<RecordType, IndexMap> {
|
||||
get(key: IDBValidKey): Promise<RecordType | undefined>;
|
||||
iter(query?: IDBValidKey): ResultStream<RecordType>;
|
||||
put(r: RecordType): Promise<InsertResponse>;
|
||||
add(r: RecordType): Promise<InsertResponse>;
|
||||
put(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>;
|
||||
add(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>;
|
||||
delete(key: IDBValidKey): Promise<void>;
|
||||
indexes: GetIndexReadWriteAccess<RecordType, IndexMap>;
|
||||
}
|
||||
@ -652,15 +655,15 @@ function makeWriteContext(
|
||||
const req = tx.objectStore(storeName).openCursor(query);
|
||||
return new ResultStream<any>(req);
|
||||
},
|
||||
async add(r) {
|
||||
const req = tx.objectStore(storeName).add(r);
|
||||
async add(r, k) {
|
||||
const req = tx.objectStore(storeName).add(r, k);
|
||||
const key = await requestToPromise(req);
|
||||
return {
|
||||
key: key,
|
||||
};
|
||||
},
|
||||
async put(r) {
|
||||
const req = tx.objectStore(storeName).put(r);
|
||||
async put(r, k) {
|
||||
const req = tx.objectStore(storeName).put(r, k);
|
||||
const key = await requestToPromise(req);
|
||||
return {
|
||||
key: key,
|
||||
|
@ -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,8 @@ import {
|
||||
clearDatabase,
|
||||
exportDb,
|
||||
importDb,
|
||||
openStoredBackupsDatabase,
|
||||
openTalerDatabase,
|
||||
} from "./db.js";
|
||||
import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
|
||||
import {
|
||||
@ -157,7 +160,6 @@ import {
|
||||
getUserAttentionsUnreadCount,
|
||||
markAttentionRequestAsRead,
|
||||
} from "./operations/attention.js";
|
||||
import { exportBackup } from "./operations/backup/export.js";
|
||||
import {
|
||||
addBackupProvider,
|
||||
codecForAddBackupProviderRequest,
|
||||
@ -165,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,
|
||||
@ -189,6 +190,7 @@ import {
|
||||
} from "./operations/deposits.js";
|
||||
import {
|
||||
acceptExchangeTermsOfService,
|
||||
addPresetExchangeEntry,
|
||||
downloadTosFromAcceptedFormat,
|
||||
getExchangeDetails,
|
||||
getExchangeRequestTimeout,
|
||||
@ -314,6 +316,7 @@ import {
|
||||
getMaxPeerPushAmount,
|
||||
convertWithdrawalAmount,
|
||||
} from "./util/instructedAmountConversion.js";
|
||||
import { IDBFactory } from "@gnu-taler/idb-bridge";
|
||||
|
||||
const logger = new Logger("wallet.ts");
|
||||
|
||||
@ -340,9 +343,8 @@ async function callOperationHandler(
|
||||
return await processRecoupGroup(ws, pending.recoupGroupId);
|
||||
case PendingTaskType.ExchangeCheckRefresh:
|
||||
return await autoRefresh(ws, pending.exchangeBaseUrl);
|
||||
case PendingTaskType.Deposit: {
|
||||
case PendingTaskType.Deposit:
|
||||
return await processDepositGroup(ws, pending.depositGroupId);
|
||||
}
|
||||
case PendingTaskType.Backup:
|
||||
return await processBackupForProvider(ws, pending.backupProviderBaseUrl);
|
||||
case PendingTaskType.PeerPushDebit:
|
||||
@ -533,6 +535,7 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
|
||||
await tx.auditorTrust.put(c);
|
||||
}
|
||||
for (const baseUrl of ws.config.builtin.exchanges) {
|
||||
await addPresetExchangeEntry(tx, baseUrl);
|
||||
const now = AbsoluteTime.now();
|
||||
provideExchangeRecordInTx(ws, tx, baseUrl, now);
|
||||
}
|
||||
@ -1021,6 +1024,23 @@ 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()}`;
|
||||
await backupsDb.mktxAll().runReadWrite(async (tx) => {
|
||||
await tx.backupMeta.add({
|
||||
name,
|
||||
});
|
||||
await tx.backupData.add(backup, name);
|
||||
});
|
||||
return {
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the "wallet-core" API.
|
||||
*/
|
||||
@ -1037,6 +1057,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;
|
||||
@ -1378,9 +1406,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);
|
||||
@ -1531,13 +1556,11 @@ 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: {
|
||||
const dbDump = await exportDb(ws.db.idbHandle());
|
||||
const dbDump = await exportDb(ws.idb);
|
||||
return dbDump;
|
||||
}
|
||||
case WalletApiOperation.ImportDb: {
|
||||
@ -1616,7 +1639,7 @@ export function getVersion(ws: InternalWalletState): WalletCoreVersion {
|
||||
/**
|
||||
* Handle a request to the wallet-core API.
|
||||
*/
|
||||
export async function handleCoreApiRequest(
|
||||
async function handleCoreApiRequest(
|
||||
ws: InternalWalletState,
|
||||
operation: string,
|
||||
id: string,
|
||||
@ -1652,14 +1675,14 @@ export class Wallet {
|
||||
private _client: WalletCoreApiClient | undefined;
|
||||
|
||||
private constructor(
|
||||
db: DbAccess<typeof WalletStoresV1>,
|
||||
idb: IDBFactory,
|
||||
http: HttpRequestLibrary,
|
||||
timer: TimerAPI,
|
||||
cryptoWorkerFactory: CryptoWorkerFactory,
|
||||
config?: WalletConfigParameter,
|
||||
) {
|
||||
this.ws = new InternalWalletStateImpl(
|
||||
db,
|
||||
idb,
|
||||
http,
|
||||
timer,
|
||||
cryptoWorkerFactory,
|
||||
@ -1675,21 +1698,20 @@ export class Wallet {
|
||||
}
|
||||
|
||||
static async create(
|
||||
db: DbAccess<typeof WalletStoresV1>,
|
||||
idb: IDBFactory,
|
||||
http: HttpRequestLibrary,
|
||||
timer: TimerAPI,
|
||||
cryptoWorkerFactory: CryptoWorkerFactory,
|
||||
config?: WalletConfigParameter,
|
||||
): Promise<Wallet> {
|
||||
const w = new Wallet(db, http, timer, cryptoWorkerFactory, config);
|
||||
const w = new Wallet(idb, http, timer, cryptoWorkerFactory, config);
|
||||
w._client = await getClientFromWalletState(w.ws);
|
||||
return w;
|
||||
}
|
||||
|
||||
public static defaultConfig: Readonly<WalletConfig> = {
|
||||
builtin: {
|
||||
//exchanges: ["https://exchange.demo.taler.net/"],
|
||||
exchanges: [],
|
||||
exchanges: ["https://exchange.demo.taler.net/"],
|
||||
auditors: [
|
||||
{
|
||||
currency: "KUDOS",
|
||||
@ -1724,19 +1746,22 @@ export class Wallet {
|
||||
this.ws.stop();
|
||||
}
|
||||
|
||||
runPending(): Promise<void> {
|
||||
async runPending(): Promise<void> {
|
||||
await this.ws.ensureWalletDbOpen();
|
||||
return runPending(this.ws);
|
||||
}
|
||||
|
||||
runTaskLoop(opts?: RetryLoopOpts): Promise<TaskLoopResult> {
|
||||
async runTaskLoop(opts?: RetryLoopOpts): Promise<TaskLoopResult> {
|
||||
await this.ws.ensureWalletDbOpen();
|
||||
return runTaskLoop(this.ws, opts);
|
||||
}
|
||||
|
||||
handleCoreApiRequest(
|
||||
async handleCoreApiRequest(
|
||||
operation: string,
|
||||
id: string,
|
||||
payload: unknown,
|
||||
): Promise<CoreApiResponse> {
|
||||
await this.ws.ensureWalletDbOpen();
|
||||
return handleCoreApiRequest(this.ws, operation, id, payload);
|
||||
}
|
||||
}
|
||||
@ -1800,12 +1825,17 @@ class InternalWalletStateImpl implements InternalWalletState {
|
||||
|
||||
config: Readonly<WalletConfig>;
|
||||
|
||||
private _db: DbAccess<typeof WalletStoresV1> | undefined = undefined;
|
||||
|
||||
get db(): DbAccess<typeof WalletStoresV1> {
|
||||
if (!this._db) {
|
||||
throw Error("db not initialized");
|
||||
}
|
||||
return this._db;
|
||||
}
|
||||
|
||||
constructor(
|
||||
// FIXME: Make this a getter and make
|
||||
// the actual value nullable.
|
||||
// Check if we are in a DB migration / garbage collection
|
||||
// and throw an error in that case.
|
||||
public db: DbAccess<typeof WalletStoresV1>,
|
||||
public idb: IDBFactory,
|
||||
public http: HttpRequestLibrary,
|
||||
public timer: TimerAPI,
|
||||
cryptoWorkerFactory: CryptoWorkerFactory,
|
||||
@ -1820,6 +1850,17 @@ class InternalWalletStateImpl implements InternalWalletState {
|
||||
}
|
||||
}
|
||||
|
||||
async ensureWalletDbOpen(): Promise<void> {
|
||||
if (this._db) {
|
||||
return;
|
||||
}
|
||||
const myVersionChange = async (): Promise<void> => {
|
||||
logger.info("version change requested for Taler DB");
|
||||
};
|
||||
const myDb = await openTalerDatabase(this.idb, myVersionChange);
|
||||
this._db = myDb;
|
||||
}
|
||||
|
||||
async getTransactionState(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadOnlyAccess<typeof WalletStoresV1>,
|
||||
|
@ -69,28 +69,28 @@ export function ShowButtonsNonAcceptedTosView({
|
||||
terms,
|
||||
}: State.ShowButtonsNotAccepted): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const ableToReviewTermsOfService =
|
||||
showingTermsOfService.button.onClick !== undefined;
|
||||
// const ableToReviewTermsOfService =
|
||||
// showingTermsOfService.button.onClick !== undefined;
|
||||
|
||||
if (!ableToReviewTermsOfService) {
|
||||
return (
|
||||
<Fragment>
|
||||
{terms.status === ExchangeTosStatus.NotFound && (
|
||||
<section style={{ justifyContent: "space-around", display: "flex" }}>
|
||||
<WarningText>
|
||||
<i18n.Translate>
|
||||
Exchange doesn't have terms of service
|
||||
</i18n.Translate>
|
||||
</WarningText>
|
||||
</section>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
// if (!ableToReviewTermsOfService) {
|
||||
// return (
|
||||
// <Fragment>
|
||||
// {terms.status === ExchangeTosStatus.Pending && (
|
||||
// <section style={{ justifyContent: "space-around", display: "flex" }}>
|
||||
// <WarningText>
|
||||
// <i18n.Translate>
|
||||
// Exchange doesn't have terms of service
|
||||
// </i18n.Translate>
|
||||
// </WarningText>
|
||||
// </section>
|
||||
// )}
|
||||
// </Fragment>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{terms.status === ExchangeTosStatus.NotFound && (
|
||||
{/* {terms.status === ExchangeTosStatus.NotFound && (
|
||||
<section style={{ justifyContent: "space-around", display: "flex" }}>
|
||||
<WarningText>
|
||||
<i18n.Translate>
|
||||
@ -98,8 +98,8 @@ export function ShowButtonsNonAcceptedTosView({
|
||||
</i18n.Translate>
|
||||
</WarningText>
|
||||
</section>
|
||||
)}
|
||||
{terms.status === "new" && (
|
||||
)} */}
|
||||
{terms.status === ExchangeTosStatus.Pending && (
|
||||
<section style={{ justifyContent: "space-around", display: "flex" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
@ -110,19 +110,6 @@ export function ShowButtonsNonAcceptedTosView({
|
||||
</Button>
|
||||
</section>
|
||||
)}
|
||||
{terms.status === "changed" && (
|
||||
<section style={{ justifyContent: "space-around", display: "flex" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={showingTermsOfService.button.onClick}
|
||||
>
|
||||
<i18n.Translate>
|
||||
Review new version of terms of service
|
||||
</i18n.Translate>
|
||||
</Button>
|
||||
</section>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@ -138,7 +125,7 @@ export function ShowTosContentView({
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{terms.status !== ExchangeTosStatus.NotFound && !terms.content && (
|
||||
{!terms.content && (
|
||||
<section style={{ justifyContent: "space-around", display: "flex" }}>
|
||||
<WarningBox>
|
||||
<i18n.Translate>
|
||||
@ -194,7 +181,7 @@ export function ShowTosContentView({
|
||||
</LinkSuccess>
|
||||
</section>
|
||||
)}
|
||||
{termsAccepted && terms.status !== ExchangeTosStatus.NotFound && (
|
||||
{termsAccepted && terms.status !== ExchangeTosStatus.Proposed && (
|
||||
<section style={{ justifyContent: "space-around", display: "flex" }}>
|
||||
<CheckboxOutlined
|
||||
name="terms"
|
||||
|
@ -141,8 +141,8 @@ export function useComponentStateFromParams({
|
||||
confirm: {
|
||||
onClick: isValid
|
||||
? pushAlertOnError(async () => {
|
||||
onAmountChanged(Amounts.stringify(amount));
|
||||
})
|
||||
onAmountChanged(Amounts.stringify(amount));
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
amount: {
|
||||
@ -304,8 +304,8 @@ function exchangeSelectionState(
|
||||
const [ageRestricted, setAgeRestricted] = useState(0);
|
||||
const currentExchange = selectedExchange.selected;
|
||||
const tosNeedToBeAccepted =
|
||||
currentExchange.tosStatus == ExchangeTosStatus.New ||
|
||||
currentExchange.tosStatus == ExchangeTosStatus.Changed;
|
||||
currentExchange.tosStatus == ExchangeTosStatus.Pending ||
|
||||
currentExchange.tosStatus == ExchangeTosStatus.Proposed;
|
||||
|
||||
/**
|
||||
* With the exchange and amount, ask the wallet the information
|
||||
@ -393,12 +393,12 @@ function exchangeSelectionState(
|
||||
//TODO: calculate based on exchange info
|
||||
const ageRestriction = ageRestrictionEnabled
|
||||
? {
|
||||
list: ageRestrictionOptions,
|
||||
value: String(ageRestricted),
|
||||
onChange: pushAlertOnError(async (v: string) =>
|
||||
setAgeRestricted(parseInt(v, 10)),
|
||||
),
|
||||
}
|
||||
list: ageRestrictionOptions,
|
||||
value: String(ageRestricted),
|
||||
onChange: pushAlertOnError(async (v: string) =>
|
||||
setAgeRestricted(parseInt(v, 10)),
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
|
@ -37,7 +37,7 @@ const exchanges: ExchangeListItem[] = [
|
||||
exchangeBaseUrl: "http://exchange.demo.taler.net",
|
||||
paytoUris: [],
|
||||
tosStatus: ExchangeTosStatus.Accepted,
|
||||
exchangeStatus: ExchangeEntryStatus.Ok,
|
||||
exchangeStatus: ExchangeEntryStatus.Used,
|
||||
permanent: true,
|
||||
auditors: [
|
||||
{
|
||||
@ -202,7 +202,7 @@ describe("Withdraw CTA states", () => {
|
||||
|
||||
const exchangeWithNewTos = exchanges.map((e) => ({
|
||||
...e,
|
||||
tosStatus: ExchangeTosStatus.New,
|
||||
tosStatus: ExchangeTosStatus.Proposed,
|
||||
}));
|
||||
|
||||
handler.addWalletCallResponse(
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
ExchangeEntryStatus,
|
||||
ExchangeListItem,
|
||||
ExchangeTosStatus,
|
||||
ExchangeUpdateStatus,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||
import { expect } from "chai";
|
||||
@ -36,9 +37,9 @@ const exchangeArs: ExchangeListItem = {
|
||||
currency: "ARS",
|
||||
exchangeBaseUrl: "http://",
|
||||
tosStatus: ExchangeTosStatus.Accepted,
|
||||
exchangeStatus: ExchangeEntryStatus.Ok,
|
||||
exchangeEntryStatus: ExchangeEntryStatus.Used,
|
||||
exchangeUpdateStatus: ExchangeUpdateStatus.Initial,
|
||||
paytoUris: [],
|
||||
permanent: true,
|
||||
ageRestrictionOptions: [],
|
||||
};
|
||||
|
||||
|
@ -163,20 +163,16 @@ export function SettingsView({
|
||||
<i18n.Translate>ok</i18n.Translate>
|
||||
</SuccessText>
|
||||
);
|
||||
case ExchangeTosStatus.Changed:
|
||||
case ExchangeTosStatus.Pending:
|
||||
return (
|
||||
<WarningText>
|
||||
<i18n.Translate>changed</i18n.Translate>
|
||||
<i18n.Translate>pending</i18n.Translate>
|
||||
</WarningText>
|
||||
);
|
||||
case ExchangeTosStatus.New:
|
||||
case ExchangeTosStatus.NotFound:
|
||||
case ExchangeTosStatus.Proposed:
|
||||
return (
|
||||
<DestructiveText>
|
||||
<i18n.Translate>not accepted</i18n.Translate>
|
||||
</DestructiveText>
|
||||
<i18n.Translate>proposed</i18n.Translate>
|
||||
);
|
||||
case ExchangeTosStatus.Unknown:
|
||||
default:
|
||||
return (
|
||||
<DestructiveText>
|
||||
|
@ -50,7 +50,6 @@ import {
|
||||
exportDb,
|
||||
importDb,
|
||||
openPromise,
|
||||
openTalerDatabase,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
import {
|
||||
MessageFromBackend,
|
||||
@ -139,7 +138,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");
|
||||
@ -298,13 +297,6 @@ async function reinitWallet(): Promise<void> {
|
||||
}
|
||||
currentDatabase = undefined;
|
||||
// setBadgeText({ text: "" });
|
||||
try {
|
||||
currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet);
|
||||
} catch (e) {
|
||||
logger.error("could not open database", e);
|
||||
walletInit.reject(e);
|
||||
return;
|
||||
}
|
||||
let httpLib;
|
||||
let cryptoWorker;
|
||||
let timer;
|
||||
@ -325,7 +317,7 @@ async function reinitWallet(): Promise<void> {
|
||||
const settings = await platform.getSettingsFromStorage();
|
||||
logger.info("Setting up wallet");
|
||||
const wallet = await Wallet.create(
|
||||
currentDatabase,
|
||||
indexedDB as any,
|
||||
httpLib,
|
||||
timer,
|
||||
cryptoWorker,
|
||||
|
@ -106,7 +106,7 @@ export function useLocalStorage<Type = string>(
|
||||
const newValue = storage.get(key.id);
|
||||
setStoredValue(convert(newValue));
|
||||
});
|
||||
}, []);
|
||||
}, [key.id]);
|
||||
|
||||
const setValue = (value?: Type): void => {
|
||||
if (value === undefined) {
|
||||
|
@ -51,7 +51,7 @@ export function useMemoryStorage<Type = string>(
|
||||
const newValue = storage.get(key);
|
||||
setStoredValue(newValue === undefined ? defaultValue : newValue);
|
||||
});
|
||||
}, []);
|
||||
}, [key]);
|
||||
|
||||
const setValue = (value?: Type): void => {
|
||||
if (value === undefined) {
|
||||
|
Loading…
Reference in New Issue
Block a user