hide internal wallet state, keep it internal to package

This commit is contained in:
Florian Dold 2021-06-17 21:06:45 +02:00
parent 954ed23911
commit 99550b0011
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 252 additions and 125 deletions

View File

@ -33,8 +33,7 @@ import {
WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION,
WALLET_MERCHANT_PROTOCOL_VERSION, WALLET_MERCHANT_PROTOCOL_VERSION,
runRetryLoop, runRetryLoop,
handleCoreApiRequest, Wallet,
InternalWalletState,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import fs from "fs"; import fs from "fs";
@ -156,8 +155,8 @@ function sendAkonoMessage(ev: CoreApiEnvelope): void {
class AndroidWalletMessageHandler { class AndroidWalletMessageHandler {
walletArgs: DefaultNodeWalletArgs | undefined; walletArgs: DefaultNodeWalletArgs | undefined;
maybeWallet: InternalWalletState | undefined; maybeWallet: Wallet | undefined;
wp = openPromise<InternalWalletState>(); wp = openPromise<Wallet>();
httpLib = new NodeHttpLib(); httpLib = new NodeHttpLib();
/** /**
@ -180,8 +179,8 @@ class AndroidWalletMessageHandler {
const reinit = async () => { const reinit = async () => {
const w = await getDefaultNodeWallet(this.walletArgs); const w = await getDefaultNodeWallet(this.walletArgs);
this.maybeWallet = w; this.maybeWallet = w;
await handleCoreApiRequest(w, "initWallet", "akono-init", {}); await w.handleCoreApiRequest("initWallet", "akono-init", {});
runRetryLoop(w).catch((e) => { w.runRetryLoop().catch((e) => {
console.error("Error during wallet retry loop", e); console.error("Error during wallet retry loop", e);
}); });
this.wp.resolve(w); this.wp.resolve(w);
@ -230,14 +229,14 @@ class AndroidWalletMessageHandler {
} }
const wallet = await this.wp.promise; const wallet = await this.wp.promise;
wallet.stop(); wallet.stop();
this.wp = openPromise<InternalWalletState>(); this.wp = openPromise<Wallet>();
this.maybeWallet = undefined; this.maybeWallet = undefined;
await reinit(); await reinit();
return wrapResponse({}); return wrapResponse({});
} }
default: { default: {
const wallet = await this.wp.promise; const wallet = await this.wp.promise;
return await handleCoreApiRequest(wallet, operation, id, args); return await wallet.handleCoreApiRequest(operation, id, args);
} }
} }
} }

View File

@ -51,7 +51,7 @@ import {
getClientFromWalletState, getClientFromWalletState,
WalletApiOperation, WalletApiOperation,
WalletCoreApiClient, WalletCoreApiClient,
InternalWalletState, Wallet,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
// This module also serves as the entry point for the crypto // This module also serves as the entry point for the crypto
@ -172,10 +172,7 @@ type WalletCliArgsType = clk.GetArgType<typeof walletCli>;
async function withWallet<T>( async function withWallet<T>(
walletCliArgs: WalletCliArgsType, walletCliArgs: WalletCliArgsType,
f: (w: { f: (w: { client: WalletCoreApiClient; ws: Wallet }) => Promise<T>,
client: WalletCoreApiClient;
ws: InternalWalletState;
}) => Promise<T>,
): Promise<T> { ): Promise<T> {
const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath; const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath;
const myHttpLib = new NodeHttpLib(); const myHttpLib = new NodeHttpLib();
@ -190,7 +187,7 @@ async function withWallet<T>(
try { try {
const w = { const w = {
ws: wallet, ws: wallet,
client: await getClientFromWalletState(wallet), client: wallet.client,
}; };
const ret = await f(w); const ret = await f(w);
return ret; return ret;
@ -242,8 +239,7 @@ walletCli
console.error("Invalid JSON"); console.error("Invalid JSON");
process.exit(1); process.exit(1);
} }
const resp = await handleCoreApiRequest( const resp = await wallet.ws.handleCoreApiRequest(
wallet.ws,
args.api.operation, args.api.operation,
"reqid-1", "reqid-1",
requestJson, requestJson,
@ -294,7 +290,7 @@ walletCli
.flag("forceNow", ["-f", "--force-now"]) .flag("forceNow", ["-f", "--force-now"])
.action(async (args) => { .action(async (args) => {
await withWallet(args, async (wallet) => { await withWallet(args, async (wallet) => {
await runPending(wallet.ws, args.runPendingOpt.forceNow); await wallet.ws.runPending(args.runPendingOpt.forceNow);
}); });
}); });
@ -318,7 +314,7 @@ walletCli
.maybeOption("maxRetries", ["--max-retries"], clk.INT) .maybeOption("maxRetries", ["--max-retries"], clk.INT)
.action(async (args) => { .action(async (args) => {
await withWallet(args, async (wallet) => { await withWallet(args, async (wallet) => {
await runUntilDone(wallet.ws, { await wallet.ws.runUntilDone({
maxRetries: args.finishPendingOpt.maxRetries, maxRetries: args.finishPendingOpt.maxRetries,
}); });
wallet.ws.stop(); wallet.ws.stop();
@ -607,7 +603,7 @@ depositCli
}, },
); );
console.log(`Created deposit ${resp.depositGroupId}`); console.log(`Created deposit ${resp.depositGroupId}`);
await runPending(wallet.ws); await wallet.ws.runPending();
}); });
}); });

View File

@ -40,8 +40,6 @@ import {
import { DbAccess, GetReadOnlyAccess } from "./util/query.js"; import { DbAccess, GetReadOnlyAccess } from "./util/query.js";
import { TimerGroup } from "./util/timer.js"; import { TimerGroup } from "./util/timer.js";
type NotificationListener = (n: WalletNotification) => void;
const logger = new Logger("state.ts"); const logger = new Logger("state.ts");
export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
@ -79,114 +77,51 @@ export interface ExchangeOperations {
}>; }>;
} }
export type NotificationListener = (n: WalletNotification) => void;
/** /**
* Internal state of the wallet. * Internal, shard wallet state that is used by the implementation
* of wallet operations.
*
* FIXME: This should not be exported anywhere from the taler-wallet-core package,
* as it's an opaque implementation detail.
*/ */
export class InternalWalletState implements InternalWalletState { export interface InternalWalletState {
memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessReserve: AsyncOpMemoMap<void>;
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoMakePlanchet: AsyncOpMemoMap<void>;
memoGetPending: AsyncOpMemoSingle<PendingOperationsResponse> = new AsyncOpMemoSingle(); memoGetPending: AsyncOpMemoSingle<PendingOperationsResponse>;
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle(); memoGetBalance: AsyncOpMemoSingle<BalancesResponse>;
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessRefresh: AsyncOpMemoMap<void>;
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessRecoup: AsyncOpMemoMap<void>;
memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessDeposit: AsyncOpMemoMap<void>;
cryptoApi: CryptoApi; cryptoApi: CryptoApi;
timerGroup: TimerGroup = new TimerGroup(); timerGroup: TimerGroup;
latch = new AsyncCondition(); latch: AsyncCondition;
stopped = false; stopped: boolean;
memoRunRetryLoop = new AsyncOpMemoSingle<void>(); memoRunRetryLoop: AsyncOpMemoSingle<void>;
listeners: NotificationListener[] = []; listeners: NotificationListener[];
initCalled: boolean = false; initCalled: boolean;
// FIXME: This should be done in wallet.ts, here we should only give declarations exchangeOps: ExchangeOperations;
exchangeOps: ExchangeOperations = {
getExchangeDetails,
getExchangeTrust,
updateExchangeFromUrl,
};
/** db: DbAccess<typeof WalletStoresV1>;
* Promises that are waiting for a particular resource. http: HttpRequestLibrary;
*/
private resourceWaiters: Record<string, OpenedPromise<void>[]> = {};
/** notify(n: WalletNotification): void;
* Resources that are currently locked.
*/
private resourceLocks: Set<string> = new Set();
constructor( addNotificationListener(f: (n: WalletNotification) => void): void;
// 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 http: HttpRequestLibrary,
cryptoWorkerFactory: CryptoWorkerFactory,
) {
this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
}
notify(n: WalletNotification): void {
logger.trace("Notification", n);
for (const l of this.listeners) {
const nc = JSON.parse(JSON.stringify(n));
setTimeout(() => {
l(nc);
}, 0);
}
}
addNotificationListener(f: (n: WalletNotification) => void): void {
this.listeners.push(f);
}
/** /**
* Stop ongoing processing. * Stop ongoing processing.
*/ */
stop(): void { stop(): void;
this.stopped = true;
this.timerGroup.stopCurrentAndFutureTimers();
this.cryptoApi.stop();
}
/** /**
* Run an async function after acquiring a list of locks, identified * Run an async function after acquiring a list of locks, identified
* by string tokens. * by string tokens.
*/ */
async runSequentialized<T>(tokens: string[], f: () => Promise<T>) { runSequentialized<T>(tokens: string[], f: () => Promise<T>): Promise<T>;
// Make sure locks are always acquired in the same order
tokens = [...tokens].sort();
for (const token of tokens) {
if (this.resourceLocks.has(token)) {
const p = openPromise<void>();
let waitList = this.resourceWaiters[token];
if (!waitList) {
waitList = this.resourceWaiters[token] = [];
}
waitList.push(p);
await p.promise;
}
this.resourceLocks.add(token);
}
try {
logger.trace(`begin exclusive execution on ${JSON.stringify(tokens)}`);
const result = await f();
logger.trace(`end exclusive execution on ${JSON.stringify(tokens)}`);
return result;
} finally {
for (const token of tokens) {
this.resourceLocks.delete(token);
let waiter = (this.resourceWaiters[token] ?? []).shift();
if (waiter) {
waiter.resolve();
}
}
}
}
} }

View File

@ -36,6 +36,7 @@ import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWor
import type { IDBFactory } from "@gnu-taler/idb-bridge"; import type { IDBFactory } from "@gnu-taler/idb-bridge";
import { WalletNotification } from "@gnu-taler/taler-util"; import { WalletNotification } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../common.js"; import { InternalWalletState } from "../common.js";
import { Wallet } from "../wallet.js";
const logger = new Logger("headless/helpers.ts"); const logger = new Logger("headless/helpers.ts");
@ -93,7 +94,7 @@ function makeId(length: number): string {
*/ */
export async function getDefaultNodeWallet( export async function getDefaultNodeWallet(
args: DefaultNodeWalletArgs = {}, args: DefaultNodeWalletArgs = {},
): Promise<InternalWalletState> { ): Promise<Wallet> {
BridgeIDBFactory.enableTracing = false; BridgeIDBFactory.enableTracing = false;
const myBackend = new MemoryBackend(); const myBackend = new MemoryBackend();
myBackend.enableTracing = false; myBackend.enableTracing = false;
@ -172,7 +173,7 @@ export async function getDefaultNodeWallet(
workerFactory = new SynchronousCryptoWorkerFactory(); workerFactory = new SynchronousCryptoWorkerFactory();
} }
const w = new InternalWalletState(myDb, myHttpLib, workerFactory); const w = await Wallet.create(myDb, myHttpLib, workerFactory);
if (args.notifyHandler) { if (args.notifyHandler) {
w.addNotificationListener(args.notifyHandler); w.addNotificationListener(args.notifyHandler);

View File

@ -23,6 +23,7 @@
* Imports. * Imports.
*/ */
import { import {
BalancesResponse,
codecForAny, codecForAny,
codecForDeleteTransactionRequest, codecForDeleteTransactionRequest,
codecForRetryTransactionRequest, codecForRetryTransactionRequest,
@ -32,9 +33,11 @@ import {
getDurationRemaining, getDurationRemaining,
isTimestampExpired, isTimestampExpired,
j2s, j2s,
PreparePayResultType,
TalerErrorCode, TalerErrorCode,
Timestamp, Timestamp,
timestampMin, timestampMin,
WalletNotification,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
addBackupProvider, addBackupProvider,
@ -59,6 +62,7 @@ import {
import { import {
acceptExchangeTermsOfService, acceptExchangeTermsOfService,
getExchangeDetails, getExchangeDetails,
getExchangeTrust,
updateExchangeFromUrl, updateExchangeFromUrl,
} from "./operations/exchanges.js"; } from "./operations/exchanges.js";
import { import {
@ -85,7 +89,11 @@ import {
getFundingPaytoUris, getFundingPaytoUris,
processReserve, processReserve,
} from "./operations/reserves.js"; } from "./operations/reserves.js";
import { InternalWalletState } from "./common.js"; import {
ExchangeOperations,
InternalWalletState,
NotificationListener,
} from "./common.js";
import { import {
runIntegrationTest, runIntegrationTest,
testPay, testPay,
@ -106,16 +114,16 @@ import {
AuditorTrustRecord, AuditorTrustRecord,
CoinSourceType, CoinSourceType,
ReserveRecordStatus, ReserveRecordStatus,
WalletStoresV1,
} from "./db.js"; } from "./db.js";
import { NotificationType } from "@gnu-taler/taler-util"; import { NotificationType } from "@gnu-taler/taler-util";
import { import {
PendingOperationInfo, PendingOperationInfo,
PendingOperationsResponse,
PendingOperationType, PendingOperationType,
} from "./pending-types.js"; } from "./pending-types.js";
import { CoinDumpJson } from "@gnu-taler/taler-util"; import { CoinDumpJson } from "@gnu-taler/taler-util";
import { import { codecForTransactionsRequest } from "@gnu-taler/taler-util";
codecForTransactionsRequest,
} from "@gnu-taler/taler-util";
import { import {
AcceptManualWithdrawalResult, AcceptManualWithdrawalResult,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
@ -151,6 +159,16 @@ import { assertUnreachable } from "./util/assertUnreachable.js";
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { setWalletDeviceId } from "./operations/backup/state.js"; import { setWalletDeviceId } from "./operations/backup/state.js";
import { WalletCoreApiClient } from "./wallet-api-types.js"; import { WalletCoreApiClient } from "./wallet-api-types.js";
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
import { CryptoApi, CryptoWorkerFactory } from "./crypto/workers/cryptoApi.js";
import { TimerGroup } from "./util/timer.js";
import {
AsyncCondition,
OpenedPromise,
openPromise,
} from "./util/promiseUtils.js";
import { DbAccess } from "./util/query.js";
import { HttpRequestLibrary } from "./util/http.js";
const builtinAuditors: AuditorTrustRecord[] = [ const builtinAuditors: AuditorTrustRecord[] = [
{ {
@ -618,7 +636,6 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
return coinsJson; return coinsJson;
} }
/** /**
* Get an API client from an internal wallet state object. * Get an API client from an internal wallet state object.
*/ */
@ -936,3 +953,178 @@ export async function handleCoreApiRequest(
} }
} }
} }
/**
* Public handle to a running wallet.
*/
export class Wallet {
private ws: InternalWalletState;
private _client: WalletCoreApiClient;
private constructor(
db: DbAccess<typeof WalletStoresV1>,
http: HttpRequestLibrary,
cryptoWorkerFactory: CryptoWorkerFactory,
) {
this.ws = new InternalWalletStateImpl(db, http, cryptoWorkerFactory);
}
get client() {
return this._client;
}
static async create(
db: DbAccess<typeof WalletStoresV1>,
http: HttpRequestLibrary,
cryptoWorkerFactory: CryptoWorkerFactory,
): Promise<Wallet> {
const w = new Wallet(db, http, cryptoWorkerFactory);
w._client = await getClientFromWalletState(w.ws);
return w;
}
addNotificationListener(f: (n: WalletNotification) => void): void {
return this.ws.addNotificationListener(f);
}
stop(): void {
this.ws.stop();
}
runRetryLoop(): Promise<void> {
return runRetryLoop(this.ws);
}
runPending(forceNow: boolean = false) {
return runPending(this.ws, forceNow);
}
runUntilDone(
req: {
maxRetries?: number;
} = {},
) {
return runUntilDone(this.ws, req);
}
handleCoreApiRequest(
operation: string,
id: string,
payload: unknown,
): Promise<CoreApiResponse> {
return handleCoreApiRequest(this.ws, operation, id, payload);
}
}
/**
* Internal state of the wallet.
*
* This ties together all the operation implementations.
*/
class InternalWalletStateImpl implements InternalWalletState {
memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoGetPending: AsyncOpMemoSingle<PendingOperationsResponse> = new AsyncOpMemoSingle();
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
cryptoApi: CryptoApi;
timerGroup: TimerGroup = new TimerGroup();
latch = new AsyncCondition();
stopped = false;
memoRunRetryLoop = new AsyncOpMemoSingle<void>();
listeners: NotificationListener[] = [];
initCalled: boolean = false;
exchangeOps: ExchangeOperations = {
getExchangeDetails,
getExchangeTrust,
updateExchangeFromUrl,
};
/**
* Promises that are waiting for a particular resource.
*/
private resourceWaiters: Record<string, OpenedPromise<void>[]> = {};
/**
* Resources that are currently locked.
*/
private resourceLocks: Set<string> = new Set();
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 http: HttpRequestLibrary,
cryptoWorkerFactory: CryptoWorkerFactory,
) {
this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
}
notify(n: WalletNotification): void {
logger.trace("Notification", n);
for (const l of this.listeners) {
const nc = JSON.parse(JSON.stringify(n));
setTimeout(() => {
l(nc);
}, 0);
}
}
addNotificationListener(f: (n: WalletNotification) => void): void {
this.listeners.push(f);
}
/**
* Stop ongoing processing.
*/
stop(): void {
this.stopped = true;
this.timerGroup.stopCurrentAndFutureTimers();
this.cryptoApi.stop();
}
/**
* Run an async function after acquiring a list of locks, identified
* by string tokens.
*/
async runSequentialized<T>(tokens: string[], f: () => Promise<T>) {
// Make sure locks are always acquired in the same order
tokens = [...tokens].sort();
for (const token of tokens) {
if (this.resourceLocks.has(token)) {
const p = openPromise<void>();
let waitList = this.resourceWaiters[token];
if (!waitList) {
waitList = this.resourceWaiters[token] = [];
}
waitList.push(p);
await p.promise;
}
this.resourceLocks.add(token);
}
try {
logger.trace(`begin exclusive execution on ${JSON.stringify(tokens)}`);
const result = await f();
logger.trace(`end exclusive execution on ${JSON.stringify(tokens)}`);
return result;
} finally {
for (const token of tokens) {
this.resourceLocks.delete(token);
let waiter = (this.resourceWaiters[token] ?? []).shift();
if (waiter) {
waiter.resolve();
}
}
}
}
}

View File

@ -37,6 +37,8 @@ import {
runRetryLoop, runRetryLoop,
handleNotifyReserve, handleNotifyReserve,
InternalWalletState, InternalWalletState,
Wallet,
WalletApiOperation,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { import {
classifyTalerUri, classifyTalerUri,
@ -52,8 +54,10 @@ import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
/** /**
* Currently active wallet instance. Might be unloaded and * Currently active wallet instance. Might be unloaded and
* re-instantiated when the database is reset. * re-instantiated when the database is reset.
*
* FIXME: Maybe move the wallet reseting into the Wallet class?
*/ */
let currentWallet: InternalWalletState | undefined; let currentWallet: Wallet | undefined;
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined; let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
@ -170,7 +174,7 @@ async function dispatch(
}; };
break; break;
} }
r = await handleCoreApiRequest(w, req.operation, req.id, req.payload); r = await w.handleCoreApiRequest(req.operation, req.id, req.payload);
break; break;
} }
} }
@ -256,7 +260,7 @@ async function reinitWallet(): Promise<void> {
} }
const http = new BrowserHttpLib(); const http = new BrowserHttpLib();
console.log("setting wallet"); console.log("setting wallet");
const wallet = new InternalWalletState( const wallet = await Wallet.create(
currentDatabase, currentDatabase,
http, http,
new BrowserCryptoWorkerFactory(), new BrowserCryptoWorkerFactory(),
@ -270,7 +274,7 @@ async function reinitWallet(): Promise<void> {
} }
} }
}); });
runRetryLoop(wallet).catch((e) => { wallet.runRetryLoop().catch((e) => {
console.log("error during wallet retry loop", e); console.log("error during wallet retry loop", e);
}); });
// Useful for debugging in the background page. // Useful for debugging in the background page.
@ -360,7 +364,8 @@ function headerListener(
if (!w) { if (!w) {
return; return;
} }
handleNotifyReserve(w); // FIXME: Is this still useful?
// handleNotifyReserve(w);
}); });
break; break;
default: default:
@ -451,4 +456,3 @@ export async function wxMain(): Promise<void> {
setupHeaderListener(); setupHeaderListener();
}); });
} }