Merge branch 'master' into age-withdraw

This commit is contained in:
Özgür Kesim 2023-08-31 13:12:06 +02:00
commit 94cfcc8750
Signed by: oec
GPG Key ID: 3D76A56D79EDD9D7
31 changed files with 697 additions and 3346 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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) => {

View File

@ -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({

View File

@ -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 {

View File

@ -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;
}

View File

@ -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.");
}

View File

@ -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();
});

View File

@ -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,

View File

@ -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,

View File

@ -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;

View File

@ -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;
});
}

View File

@ -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}'`);
}
}
});
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;

View File

@ -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);
}

View File

@ -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();

View File

@ -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,

View File

@ -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);
}

View File

@ -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,

View File

@ -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<

View File

@ -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>,

View File

@ -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&apos;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&apos;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"

View File

@ -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 {

View File

@ -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(

View File

@ -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: [],
};

View File

@ -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>

View File

@ -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,

View File

@ -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) {

View File

@ -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) {