wallet-core: implement enabling/disabling dev mode

This commit is contained in:
Florian Dold 2022-10-12 22:27:50 +02:00
parent 3da1e82a24
commit ded00b680a
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
9 changed files with 156 additions and 21 deletions

View File

@ -605,6 +605,7 @@ export interface WalletCoreVersion {
exchange: string; exchange: string;
merchant: string; merchant: string;
bank: string; bank: string;
devMode?: boolean;
} }
export interface KnownBankAccountsInfo { export interface KnownBankAccountsInfo {

View File

@ -1240,7 +1240,11 @@ export interface PurchaseRecord {
refundAmountAwaiting: AmountJson | undefined; refundAmountAwaiting: AmountJson | undefined;
} }
export const WALLET_BACKUP_STATE_KEY = "walletBackupState"; export enum ConfigRecordKey {
WalletBackupState = "walletBackupState",
CurrencyDefaultsApplied = "currencyDefaultsApplied",
DevMode = "devMode",
}
/** /**
* Configuration key/value entries to configure * Configuration key/value entries to configure
@ -1248,10 +1252,11 @@ export const WALLET_BACKUP_STATE_KEY = "walletBackupState";
*/ */
export type ConfigRecord = export type ConfigRecord =
| { | {
key: typeof WALLET_BACKUP_STATE_KEY; key: ConfigRecordKey.WalletBackupState;
value: WalletBackupConfState; value: WalletBackupConfState;
} }
| { key: "currencyDefaultsApplied"; value: boolean }; | { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean }
| { key: ConfigRecordKey.DevMode; value: boolean };
export interface WalletBackupConfState { export interface WalletBackupConfState {
deviceId: string; deviceId: string;

View File

@ -25,14 +25,130 @@
* Imports. * Imports.
*/ */
import { Logger } from "@gnu-taler/taler-util"; import { Logger, parseDevExperimentUri } from "@gnu-taler/taler-util";
import { ConfigRecordKey } from "./db.js";
import { InternalWalletState } from "./internal-wallet-state.js"; import { InternalWalletState } from "./internal-wallet-state.js";
import {
HttpRequestLibrary,
HttpRequestOptions,
HttpResponse,
} from "./util/http.js";
const logger = new Logger("dev-experiments.ts"); const logger = new Logger("dev-experiments.ts");
/**
* Apply a dev experiment to the wallet database / state.
*/
export async function applyDevExperiment( export async function applyDevExperiment(
ws: InternalWalletState, ws: InternalWalletState,
uri: string, uri: string,
): Promise<void> { ): Promise<void> {
logger.info(`applying dev experiment ${uri}`); logger.info(`applying dev experiment ${uri}`);
const parsedUri = parseDevExperimentUri(uri);
if (!parsedUri) {
logger.info("unable to parse dev experiment URI");
return;
}
if (parsedUri.devExperimentId == "enable-devmode") {
logger.info("enabling devmode");
await ws.db
.mktx((x) => [x.config])
.runReadWrite(async (tx) => {
tx.config.put({
key: ConfigRecordKey.DevMode,
value: true,
});
});
await maybeInitDevMode(ws);
return;
}
if (parsedUri.devExperimentId === "disable-devmode") {
logger.info("disabling devmode");
await ws.db
.mktx((x) => [x.config])
.runReadWrite(async (tx) => {
tx.config.put({
key: ConfigRecordKey.DevMode,
value: false,
});
});
await leaveDevMode(ws);
return;
}
if (!ws.devModeActive) {
throw Error(
"can't handle devmode URI (other than enable-devmode) unless devmode is active",
);
}
throw Error(`dev-experiment id not understood ${parsedUri.devExperimentId}`);
}
/**
* Enter dev mode, if the wallet's config entry in the DB demands it.
*/
export async function maybeInitDevMode(ws: InternalWalletState): Promise<void> {
const devMode = await ws.db
.mktx((x) => [x.config])
.runReadOnly(async (tx) => {
const rec = await tx.config.get(ConfigRecordKey.DevMode);
if (!rec || rec.key !== ConfigRecordKey.DevMode) {
return false;
}
return rec.value;
});
if (!devMode) {
ws.devModeActive = false;
return;
}
ws.devModeActive = true;
if (ws.http instanceof DevExperimentHttpLib) {
return;
}
ws.http = new DevExperimentHttpLib(ws.http);
}
export async function leaveDevMode(ws: InternalWalletState): Promise<void> {
if (ws.http instanceof DevExperimentHttpLib) {
ws.http = ws.http.underlyingLib;
}
ws.devModeActive = false;
}
export class DevExperimentHttpLib implements HttpRequestLibrary {
_isDevExperimentLib = true;
underlyingLib: HttpRequestLibrary;
constructor(lib: HttpRequestLibrary) {
this.underlyingLib = lib;
}
get(
url: string,
opt?: HttpRequestOptions | undefined,
): Promise<HttpResponse> {
return this.fetch(url, {
method: "GET",
...opt,
});
}
postJson(
url: string,
body: any,
opt?: HttpRequestOptions | undefined,
): Promise<HttpResponse> {
return this.fetch(url, {
method: "POST",
body,
...opt,
});
}
fetch(
url: string,
opt?: HttpRequestOptions | undefined,
): Promise<HttpResponse> {
logger.info(`devexperiment httplib ${url}`);
return this.underlyingLib.fetch(url, opt);
}
} }

View File

@ -191,6 +191,8 @@ export interface InternalWalletState {
merchantOps: MerchantOperations; merchantOps: MerchantOperations;
refreshOps: RefreshOperations; refreshOps: RefreshOperations;
devModeActive: boolean;
getDenomInfo( getDenomInfo(
ws: InternalWalletState, ws: InternalWalletState,
tx: GetReadOnlyAccess<{ tx: GetReadOnlyAccess<{

View File

@ -64,11 +64,11 @@ import {
import { import {
CoinSourceType, CoinSourceType,
CoinStatus, CoinStatus,
ConfigRecordKey,
DenominationRecord, DenominationRecord,
PurchaseStatus, PurchaseStatus,
RefreshCoinStatus, RefreshCoinStatus,
RefundState, RefundState,
WALLET_BACKUP_STATE_KEY,
WithdrawalGroupStatus, WithdrawalGroupStatus,
WithdrawalRecordType, WithdrawalRecordType,
} from "../../db.js"; } from "../../db.js";
@ -547,7 +547,7 @@ export async function exportBackup(
)} and nonce to ${bs.lastBackupNonce}`, )} and nonce to ${bs.lastBackupNonce}`,
); );
await tx.config.put({ await tx.config.put({
key: WALLET_BACKUP_STATE_KEY, key: ConfigRecordKey.WalletBackupState,
value: bs, value: bs,
}); });
} else { } else {

View File

@ -74,8 +74,8 @@ import {
BackupProviderStateTag, BackupProviderStateTag,
BackupProviderTerms, BackupProviderTerms,
ConfigRecord, ConfigRecord,
ConfigRecordKey,
WalletBackupConfState, WalletBackupConfState,
WALLET_BACKUP_STATE_KEY,
} from "../../db.js"; } from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js"; import { InternalWalletState } from "../../internal-wallet-state.js";
import { import {
@ -861,10 +861,12 @@ async function backupRecoveryTheirs(
.mktx((x) => [x.config, x.backupProviders]) .mktx((x) => [x.config, x.backupProviders])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
let backupStateEntry: ConfigRecord | undefined = await tx.config.get( let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
WALLET_BACKUP_STATE_KEY, ConfigRecordKey.WalletBackupState,
); );
checkDbInvariant(!!backupStateEntry); checkDbInvariant(!!backupStateEntry);
checkDbInvariant(backupStateEntry.key === WALLET_BACKUP_STATE_KEY); checkDbInvariant(
backupStateEntry.key === ConfigRecordKey.WalletBackupState,
);
backupStateEntry.value.lastBackupNonce = undefined; backupStateEntry.value.lastBackupNonce = undefined;
backupStateEntry.value.lastBackupTimestamp = undefined; backupStateEntry.value.lastBackupTimestamp = undefined;
backupStateEntry.value.lastBackupCheckTimestamp = undefined; backupStateEntry.value.lastBackupCheckTimestamp = undefined;

View File

@ -17,9 +17,9 @@
import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"; import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
import { import {
ConfigRecord, ConfigRecord,
ConfigRecordKey,
WalletBackupConfState, WalletBackupConfState,
WalletStoresV1, WalletStoresV1,
WALLET_BACKUP_STATE_KEY,
} from "../../db.js"; } from "../../db.js";
import { checkDbInvariant } from "../../util/invariants.js"; import { checkDbInvariant } from "../../util/invariants.js";
import { GetReadOnlyAccess } from "../../util/query.js"; import { GetReadOnlyAccess } from "../../util/query.js";
@ -31,10 +31,10 @@ export async function provideBackupState(
const bs: ConfigRecord | undefined = await ws.db const bs: ConfigRecord | undefined = await ws.db
.mktx((stores) => [stores.config]) .mktx((stores) => [stores.config])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return await tx.config.get(WALLET_BACKUP_STATE_KEY); return await tx.config.get(ConfigRecordKey.WalletBackupState);
}); });
if (bs) { if (bs) {
checkDbInvariant(bs.key === WALLET_BACKUP_STATE_KEY); checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
return bs.value; return bs.value;
} }
// We need to generate the key outside of the transaction // We need to generate the key outside of the transaction
@ -48,11 +48,11 @@ export async function provideBackupState(
.mktx((x) => [x.config]) .mktx((x) => [x.config])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
let backupStateEntry: ConfigRecord | undefined = await tx.config.get( let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
WALLET_BACKUP_STATE_KEY, ConfigRecordKey.WalletBackupState,
); );
if (!backupStateEntry) { if (!backupStateEntry) {
backupStateEntry = { backupStateEntry = {
key: WALLET_BACKUP_STATE_KEY, key: ConfigRecordKey.WalletBackupState,
value: { value: {
deviceId, deviceId,
walletRootPub: k.pub, walletRootPub: k.pub,
@ -62,7 +62,7 @@ export async function provideBackupState(
}; };
await tx.config.put(backupStateEntry); await tx.config.put(backupStateEntry);
} }
checkDbInvariant(backupStateEntry.key === WALLET_BACKUP_STATE_KEY); checkDbInvariant(backupStateEntry.key === ConfigRecordKey.WalletBackupState);
return backupStateEntry.value; return backupStateEntry.value;
}); });
} }
@ -71,9 +71,9 @@ export async function getWalletBackupState(
ws: InternalWalletState, ws: InternalWalletState,
tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>, tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
): Promise<WalletBackupConfState> { ): Promise<WalletBackupConfState> {
const bs = await tx.config.get(WALLET_BACKUP_STATE_KEY); const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
checkDbInvariant(!!bs, "wallet backup state should be in DB"); checkDbInvariant(!!bs, "wallet backup state should be in DB");
checkDbInvariant(bs.key === WALLET_BACKUP_STATE_KEY); checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
return bs.value; return bs.value;
} }
@ -86,11 +86,11 @@ export async function setWalletDeviceId(
.mktx((x) => [x.config]) .mktx((x) => [x.config])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
let backupStateEntry: ConfigRecord | undefined = await tx.config.get( let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
WALLET_BACKUP_STATE_KEY, ConfigRecordKey.WalletBackupState,
); );
if ( if (
!backupStateEntry || !backupStateEntry ||
backupStateEntry.key !== WALLET_BACKUP_STATE_KEY backupStateEntry.key !== ConfigRecordKey.WalletBackupState
) { ) {
return; return;
} }

View File

@ -111,11 +111,15 @@ export class Headers {
export interface HttpRequestLibrary { export interface HttpRequestLibrary {
/** /**
* Make an HTTP GET request. * Make an HTTP GET request.
*
* FIXME: Get rid of this, we want the API surface to be minimal.
*/ */
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>; get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
/** /**
* Make an HTTP POST request with a JSON body. * Make an HTTP POST request with a JSON body.
*
* FIXME: Get rid of this, we want the API surface to be minimal.
*/ */
postJson( postJson(
url: string, url: string,

View File

@ -105,12 +105,13 @@ import {
AuditorTrustRecord, AuditorTrustRecord,
CoinSourceType, CoinSourceType,
CoinStatus, CoinStatus,
ConfigRecordKey,
DenominationRecord, DenominationRecord,
exportDb, exportDb,
importDb, importDb,
WalletStoresV1, WalletStoresV1,
} from "./db.js"; } from "./db.js";
import { applyDevExperiment } from "./dev-experiments.js"; import { applyDevExperiment, maybeInitDevMode } from "./dev-experiments.js";
import { getErrorDetailFromException, TalerError } from "./errors.js"; import { getErrorDetailFromException, TalerError } from "./errors.js";
import { import {
ActiveLongpollInfo, ActiveLongpollInfo,
@ -476,7 +477,7 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
provideExchangeRecordInTx(ws, tx, baseUrl, now); provideExchangeRecordInTx(ws, tx, baseUrl, now);
} }
await tx.config.put({ await tx.config.put({
key: "currencyDefaultsApplied", key: ConfigRecordKey.CurrencyDefaultsApplied,
value: true, value: true,
}); });
}); });
@ -970,6 +971,7 @@ async function dispatchRequestInternal(
logger.trace("filling defaults"); logger.trace("filling defaults");
await fillDefaults(ws); await fillDefaults(ws);
} }
await maybeInitDevMode(ws);
return {}; return {};
} }
case "withdrawTestkudos": { case "withdrawTestkudos": {
@ -1339,6 +1341,7 @@ async function dispatchRequestInternal(
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION, merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
devMode: ws.devModeActive,
}; };
return version; return version;
} }
@ -1480,6 +1483,8 @@ class InternalWalletStateImpl implements InternalWalletState {
initCalled = false; initCalled = false;
devModeActive = false;
exchangeOps: ExchangeOperations = { exchangeOps: ExchangeOperations = {
getExchangeDetails, getExchangeDetails,
getExchangeTrust, getExchangeTrust,