From 2adac7c9ec52bc5a8b908ddfd692eca8919563d6 Mon Sep 17 00:00:00 2001 From: okjodom Date: Fri, 4 Oct 2024 13:36:02 +0300 Subject: [PATCH 1/4] feat: create abstract client base with web worker - this provides common instantiation and state observation logic for when we create a fedimint clint like FedimintWallet --- packages/core-web/src/Base.ts | 98 +++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 packages/core-web/src/Base.ts diff --git a/packages/core-web/src/Base.ts b/packages/core-web/src/Base.ts new file mode 100644 index 0000000..8328296 --- /dev/null +++ b/packages/core-web/src/Base.ts @@ -0,0 +1,98 @@ +import { WorkerClient } from './worker/WorkerClient' +import { logger, type LogLevel } from './utils/logger' + +export abstract class Base { + protected client: WorkerClient + + private _openPromise: Promise | null = null + private _resolveOpen: () => void = () => {} + private _isOpen: boolean = false + + constructor(client?: WorkerClient, lazy: boolean = false) { + this._openPromise = new Promise((resolve) => { + this._resolveOpen = resolve + }) + + if (client) { + this.client = client + logger.info( + `${this.constructor.name} instantiated with provided WorkerClient`, + ) + } else { + this.client = new WorkerClient() + logger.info(`${this.constructor.name} instantiated with new WorkerClient`) + } + + if (!lazy) { + this.initialize() + } + } + + async initialize() { + logger.info('Initializing WorkerClient') + await this.client.initialize() + logger.info('WorkerClient initialized') + } + + async waitForOpen() { + if (this._isOpen) return Promise.resolve() + return this._openPromise + } + + isOpen() { + return this._isOpen + } + + protected setOpen(isOpen: boolean) { + this._isOpen = isOpen + if (isOpen) { + this._resolveOpen() + } + } + + async open(clientName: string) { + await this.initialize() + if (this.isOpen()) throw new Error('already open.') + const { success } = await this.client.sendSingleMessage('open', { + clientName, + }) + if (success) { + this.setOpen(true) + } + return success + } + + async joinFederation(inviteCode: string, clientName: string) { + await this.initialize() + if (this.isOpen()) + throw new Error( + 'already open. You can only call `joinFederation` on closed clients.', + ) + const response = await this.client.sendSingleMessage('join', { + inviteCode, + clientName, + }) + if (response.success) { + this.setOpen(true) + } + } + + /** + * This should ONLY be called when UNLOADING the client. + * After this call, the instance should be discarded. + */ + async cleanup() { + this._openPromise = null + this._isOpen = false + this.client.cleanup() + } + + /** + * Sets the log level for the library. + * @param level The desired log level ('DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'). + */ + setLogLevel(level: LogLevel) { + logger.setLevel(level) + logger.info(`Log level set to ${level}.`) + } +} From 736fd5fb0f9f5db963f42cade3abdb87a78c51f6 Mon Sep 17 00:00:00 2001 From: okjodom Date: Fri, 4 Oct 2024 13:38:49 +0300 Subject: [PATCH 2/4] feat: declare standalone Federation client logic - an instance of this class can be used to interact with the federation, without any of the other wallet or lightnign modules - should probably be called `FederationAdmin`? --- .../src/federation/Federation.test.ts | 99 +++++++++++++++++++ .../core-web/src/federation/Federation.ts | 38 +++++++ packages/core-web/src/federation/index.ts | 1 + 3 files changed, 138 insertions(+) create mode 100644 packages/core-web/src/federation/Federation.test.ts create mode 100644 packages/core-web/src/federation/Federation.ts create mode 100644 packages/core-web/src/federation/index.ts diff --git a/packages/core-web/src/federation/Federation.test.ts b/packages/core-web/src/federation/Federation.test.ts new file mode 100644 index 0000000..d61ac83 --- /dev/null +++ b/packages/core-web/src/federation/Federation.test.ts @@ -0,0 +1,99 @@ +import { expect, test } from 'vitest' + +import { Federation } from './Federation' +import { WorkerClient } from '../worker/WorkerClient' +import { TestingService } from '../test/TestingService' + +class TestFederation extends Federation { + public testing: TestingService + + constructor() { + super() + this.testing = new TestingService(this.getWorkerClient()) + } + + // Method to expose the WorkerClient + getWorkerClient(): WorkerClient { + return this['client'] + } +} + +/** + * Adds Fixtures for setting up and tearing down a test Fedimintfederation instance + */ +const federationTest = test.extend<{ federation: TestFederation }>({ + federation: async ({}, use) => { + const randomTestingId = Math.random().toString(36).substring(2, 15) + const federation = new TestFederation() + expect(federation).toBeDefined() + + await expect( + federation.joinFederation( + federation.testing.TESTING_INVITE, + randomTestingId, + ), + ).resolves.toBeUndefined() + await use(federation) + + // clear up browser resources + await federation.cleanup() + // remove the federation db + indexedDB.deleteDatabase(randomTestingId) + }, +}) + +federationTest( + 'getConfig should return the federation config', + async ({ federation }) => { + expect(federation).toBeDefined() + expect(federation.isOpen()).toBe(true) + const counterBefore = federation.testing.getRequestCounter() + await expect(federation.getConfig()).resolves.toMatchObject({ + api_endpoints: expect.any(Object), + broadcast_public_keys: expect.any(Object), + consensus_version: expect.any(Object), + meta: expect.any(Object), + modules: expect.any(Object), + }) + expect(federation.testing.getRequestCounter()).toBe(counterBefore + 1) + }, +) + +federationTest( + 'getFederationId should return the federation id', + async ({ federation }) => { + expect(federation).toBeDefined() + expect(federation.isOpen()).toBe(true) + + const counterBefore = federation.testing.getRequestCounter() + const federationId = await federation.getFederationId() + expect(federationId).toBeTypeOf('string') + expect(federationId).toHaveLength(64) + expect(federation.testing.getRequestCounter()).toBe(counterBefore + 1) + }, +) + +federationTest( + 'getInviteCode should return the invite code', + async ({ federation }) => { + expect(federation).toBeDefined() + expect(federation.isOpen()).toBe(true) + + const counterBefore = federation.testing.getRequestCounter() + const inviteCode = await federation.getInviteCode(0) + expect(inviteCode).toBeTypeOf('string') + expect(federation.testing.getRequestCounter()).toBe(counterBefore + 1) + }, +) + +federationTest( + 'listOperations should return the list of operations', + async ({ federation }) => { + expect(federation).toBeDefined() + expect(federation.isOpen()).toBe(true) + + const counterBefore = federation.testing.getRequestCounter() + await expect(federation.listOperations()).resolves.toMatchObject([]) + expect(federation.testing.getRequestCounter()).toBe(counterBefore + 1) + }, +) diff --git a/packages/core-web/src/federation/Federation.ts b/packages/core-web/src/federation/Federation.ts new file mode 100644 index 0000000..3bcaa2c --- /dev/null +++ b/packages/core-web/src/federation/Federation.ts @@ -0,0 +1,38 @@ +import { JSONValue } from '../types/wallet' +import { WorkerClient } from '../worker' +import { Base } from '../Base' + +const DEFAULT_CLIENT_NAME = 'fm-federation-client' as const + +export class Federation extends Base { + constructor(client?: WorkerClient, lazy: boolean = false) { + super(client, lazy) + } + + async open(clientName: string = DEFAULT_CLIENT_NAME): Promise { + return super.open(clientName) + } + + async joinFederation( + inviteCode: string, + clientName: string = DEFAULT_CLIENT_NAME, + ): Promise { + return super.joinFederation(inviteCode, clientName) + } + + async getConfig(): Promise { + return await this.client.rpcSingle('', 'get_config', {}) + } + + async getFederationId(): Promise { + return await this.client.rpcSingle('', 'get_federation_id', {}) + } + + async getInviteCode(peer: number): Promise { + return await this.client.rpcSingle('', 'get_invite_code', { peer }) + } + + async listOperations(): Promise { + return await this.client.rpcSingle('', 'list_operations', {}) + } +} diff --git a/packages/core-web/src/federation/index.ts b/packages/core-web/src/federation/index.ts new file mode 100644 index 0000000..770bef7 --- /dev/null +++ b/packages/core-web/src/federation/index.ts @@ -0,0 +1 @@ +export { Federation } from './Federation' From 7f19678da78e2e619a23af02d5c8fbc23b4d90f3 Mon Sep 17 00:00:00 2001 From: okjodom Date: Fri, 4 Oct 2024 13:49:44 +0300 Subject: [PATCH 3/4] feat: compose Federation class into FedimintWallet - Replaces `FederationService` with an instance of `Federation` within `FedimintWallet`. - Showcases how standalone client instances can be composed into a logic set like the wallet, while sharing an indemponent `WorkerClient` instance --- packages/core-web/src/FedimintWallet.ts | 6 +- .../src/services/FederationService.test.ts | 58 ------------------- .../src/services/FederationService.ts | 22 ------- packages/core-web/src/services/index.ts | 1 - 4 files changed, 3 insertions(+), 84 deletions(-) delete mode 100644 packages/core-web/src/services/FederationService.test.ts delete mode 100644 packages/core-web/src/services/FederationService.ts diff --git a/packages/core-web/src/FedimintWallet.ts b/packages/core-web/src/FedimintWallet.ts index ab2de2a..28f1e47 100644 --- a/packages/core-web/src/FedimintWallet.ts +++ b/packages/core-web/src/FedimintWallet.ts @@ -3,10 +3,10 @@ import { BalanceService, MintService, LightningService, - FederationService, RecoveryService, } from './services' import { logger, type LogLevel } from './utils/logger' +import { Federation } from './federation' const DEFAULT_CLIENT_NAME = 'fm-default' as const @@ -16,7 +16,7 @@ export class FedimintWallet { public balance: BalanceService public mint: MintService public lightning: LightningService - public federation: FederationService + public federation: Federation public recovery: RecoveryService private _openPromise: Promise | null = null @@ -60,7 +60,7 @@ export class FedimintWallet { this.mint = new MintService(this._client) this.lightning = new LightningService(this._client) this.balance = new BalanceService(this._client) - this.federation = new FederationService(this._client) + this.federation = new Federation(this._client) this.recovery = new RecoveryService(this._client) logger.info('FedimintWallet instantiated') diff --git a/packages/core-web/src/services/FederationService.test.ts b/packages/core-web/src/services/FederationService.test.ts deleted file mode 100644 index d46ad5c..0000000 --- a/packages/core-web/src/services/FederationService.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect } from 'vitest' -import { walletTest } from '../test/setupTests' - -walletTest( - 'getConfig should return the federation config', - async ({ wallet }) => { - expect(wallet).toBeDefined() - expect(wallet.isOpen()).toBe(true) - const counterBefore = wallet.testing.getRequestCounter() - await expect(wallet.federation.getConfig()).resolves.toMatchObject({ - api_endpoints: expect.any(Object), - broadcast_public_keys: expect.any(Object), - consensus_version: expect.any(Object), - meta: expect.any(Object), - modules: expect.any(Object), - }) - expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1) - }, -) - -walletTest( - 'getFederationId should return the federation id', - async ({ wallet }) => { - expect(wallet).toBeDefined() - expect(wallet.isOpen()).toBe(true) - - const counterBefore = wallet.testing.getRequestCounter() - const federationId = await wallet.federation.getFederationId() - expect(federationId).toBeTypeOf('string') - expect(federationId).toHaveLength(64) - expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1) - }, -) - -walletTest( - 'getInviteCode should return the invite code', - async ({ wallet }) => { - expect(wallet).toBeDefined() - expect(wallet.isOpen()).toBe(true) - - const counterBefore = wallet.testing.getRequestCounter() - const inviteCode = await wallet.federation.getInviteCode(0) - expect(inviteCode).toBeTypeOf('string') - expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1) - }, -) - -walletTest( - 'listOperations should return the list of operations', - async ({ wallet }) => { - expect(wallet).toBeDefined() - expect(wallet.isOpen()).toBe(true) - - const counterBefore = wallet.testing.getRequestCounter() - await expect(wallet.federation.listOperations()).resolves.toMatchObject([]) - expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1) - }, -) diff --git a/packages/core-web/src/services/FederationService.ts b/packages/core-web/src/services/FederationService.ts deleted file mode 100644 index 6c17207..0000000 --- a/packages/core-web/src/services/FederationService.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { JSONValue } from '../types/wallet' -import { WorkerClient } from '../worker' - -export class FederationService { - constructor(private client: WorkerClient) {} - - async getConfig(): Promise { - return await this.client.rpcSingle('', 'get_config', {}) - } - - async getFederationId(): Promise { - return await this.client.rpcSingle('', 'get_federation_id', {}) - } - - async getInviteCode(peer: number): Promise { - return await this.client.rpcSingle('', 'get_invite_code', { peer }) - } - - async listOperations(): Promise { - return await this.client.rpcSingle('', 'list_operations', {}) - } -} diff --git a/packages/core-web/src/services/index.ts b/packages/core-web/src/services/index.ts index f5318a2..5563568 100644 --- a/packages/core-web/src/services/index.ts +++ b/packages/core-web/src/services/index.ts @@ -2,4 +2,3 @@ export { MintService } from './MintService' export { BalanceService } from './BalanceService' export { LightningService } from './LightningService' export { RecoveryService } from './RecoveryService' -export { FederationService } from './FederationService' From 0c0b088fbf1d9b77f637f7f89267a7ea5a2bd88b Mon Sep 17 00:00:00 2001 From: okjodom Date: Fri, 4 Oct 2024 13:55:55 +0300 Subject: [PATCH 4/4] chore: declare changeset --- .changeset/tough-peas-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tough-peas-drop.md diff --git a/.changeset/tough-peas-drop.md b/.changeset/tough-peas-drop.md new file mode 100644 index 0000000..54bdbc9 --- /dev/null +++ b/.changeset/tough-peas-drop.md @@ -0,0 +1,5 @@ +--- +'@fedimint/core-web': patch +--- + +continue modularizing client-web, with standalone and composable components sharing the same web worker resources