From 7a97d94666080a42aac3abb318e70e10031d3319 Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Mon, 21 Oct 2024 14:28:23 -0400 Subject: [PATCH] refactor(liquid): implement dynamic import for LiquidAuthClient (#299) * feat(liquid): add logging - Integrate `logger` into `LiquidWallet` for improved debugging and monitoring - Update tests to mock `logger` * refactor(liquid): update signTransactions method - Add debug logging for transaction signing process - Implement try-catch block for error handling - Remove underscore prefix from method parameters * refactor(liquid): add createWalletWithStore to tests * refactor(liquid): implement dynamic import for LiquidAuthClient - Remove static import of `LiquidAuthClient` - Add `initializeClient` method for lazy-loading - Update `connect` and `signTransactions` to use lazy-loaded client - Inline `ICON` constant to remove static dependency - Improve error handling and logging - Update tests to reflect new behavior * chore: add LiquidAuthClient dependency to example apps - Install `@algorandfoundation/liquid-auth-use-wallet-client` in example apps - Add fallback for this optional dependency in Next.js config * fix(liquid): convert public methods to arrow functions Ensures correct 'this' context when methods are used as callbacks, addressing issues like undefined 'this.store' in React components. Aligns with the fix implemented in PR #152. See: https://github.com/txnlab/use-wallet/pull/152 * refactor(examples): simplify LiquidWallet configuration Update example apps to use enum syntax for `LiquidWallet` when no custom options are needed. --- examples/nextjs/next.config.mjs | 1 + examples/nuxt/package.json | 1 + examples/nuxt/plugins/walletManager.client.ts | 4 +- examples/react-ts/package.json | 1 + examples/react-ts/src/App.tsx | 4 +- examples/solid-ts/package.json | 1 + examples/solid-ts/src/App.tsx | 4 +- examples/vanilla-ts/package.json | 1 + examples/vanilla-ts/src/main.ts | 4 +- examples/vue-ts/package.json | 1 + examples/vue-ts/src/main.ts | 4 +- .../src/__tests__/wallets/liquid.test.ts | 81 +++++++++++++--- packages/use-wallet/src/wallets/liquid.ts | 96 ++++++++++++++----- pnpm-lock.yaml | 19 +++- 14 files changed, 167 insertions(+), 55 deletions(-) diff --git a/examples/nextjs/next.config.mjs b/examples/nextjs/next.config.mjs index be780467..7d9949ee 100644 --- a/examples/nextjs/next.config.mjs +++ b/examples/nextjs/next.config.mjs @@ -15,6 +15,7 @@ const nextConfig = { config.resolve.fallback = { ...config.resolve.fallback, '@agoralabs-sh/avm-web-provider': false, + '@algorandfoundation/liquid-auth-use-wallet-client': false, '@blockshake/defly-connect': false, '@magic-ext/algorand': false, '@perawallet/connect': false, diff --git a/examples/nuxt/package.json b/examples/nuxt/package.json index 1a89a8f4..93a7e7c4 100644 --- a/examples/nuxt/package.json +++ b/examples/nuxt/package.json @@ -11,6 +11,7 @@ "typecheck": "npx nuxi typecheck" }, "dependencies": { + "@algorandfoundation/liquid-auth-use-wallet-client": "1.1.0", "@blockshake/defly-connect": "^1.1.6", "@perawallet/connect": "^1.3.4", "@txnlab/use-wallet": "workspace:*", diff --git a/examples/nuxt/plugins/walletManager.client.ts b/examples/nuxt/plugins/walletManager.client.ts index e7e6b305..1333131a 100644 --- a/examples/nuxt/plugins/walletManager.client.ts +++ b/examples/nuxt/plugins/walletManager.client.ts @@ -17,9 +17,7 @@ export default defineNuxtPlugin((nuxtApp) => { }, WalletId.KMD, WalletId.KIBISIS, - { - id: WalletId.LIQUID - }, + WalletId.LIQUID, { id: WalletId.LUTE, options: { siteName: 'Example Site' } diff --git a/examples/react-ts/package.json b/examples/react-ts/package.json index 236e630b..9ae004f1 100644 --- a/examples/react-ts/package.json +++ b/examples/react-ts/package.json @@ -9,6 +9,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@algorandfoundation/liquid-auth-use-wallet-client": "1.1.0", "@blockshake/defly-connect": "^1.1.6", "@perawallet/connect": "^1.3.4", "@txnlab/use-wallet-react": "workspace:*", diff --git a/examples/react-ts/src/App.tsx b/examples/react-ts/src/App.tsx index f3002f83..7ded7944 100644 --- a/examples/react-ts/src/App.tsx +++ b/examples/react-ts/src/App.tsx @@ -19,9 +19,7 @@ const walletManager = new WalletManager({ }, WalletId.KMD, WalletId.KIBISIS, - { - id: WalletId.LIQUID - }, + WalletId.LIQUID, { id: WalletId.LUTE, options: { siteName: 'Example Site' } diff --git a/examples/solid-ts/package.json b/examples/solid-ts/package.json index 40308bb3..63d8a77c 100644 --- a/examples/solid-ts/package.json +++ b/examples/solid-ts/package.json @@ -10,6 +10,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@algorandfoundation/liquid-auth-use-wallet-client": "1.1.0", "@blockshake/defly-connect": "^1.1.6", "@perawallet/connect": "^1.3.4", "@txnlab/use-wallet-solid": "workspace:*", diff --git a/examples/solid-ts/src/App.tsx b/examples/solid-ts/src/App.tsx index 7b4a3df3..7b66aa82 100644 --- a/examples/solid-ts/src/App.tsx +++ b/examples/solid-ts/src/App.tsx @@ -19,9 +19,7 @@ const walletManager = new WalletManager({ }, WalletId.KMD, WalletId.KIBISIS, - { - id: WalletId.LIQUID - }, + WalletId.LIQUID, { id: WalletId.LUTE, options: { siteName: 'Example Site' } diff --git a/examples/vanilla-ts/package.json b/examples/vanilla-ts/package.json index dba6b5e2..40d3904b 100644 --- a/examples/vanilla-ts/package.json +++ b/examples/vanilla-ts/package.json @@ -14,6 +14,7 @@ "vite": "5.4.8" }, "dependencies": { + "@algorandfoundation/liquid-auth-use-wallet-client": "1.1.0", "@blockshake/defly-connect": "^1.1.6", "@perawallet/connect": "^1.3.4", "@txnlab/use-wallet": "workspace:*", diff --git a/examples/vanilla-ts/src/main.ts b/examples/vanilla-ts/src/main.ts index 30d260f5..36b8254c 100644 --- a/examples/vanilla-ts/src/main.ts +++ b/examples/vanilla-ts/src/main.ts @@ -20,9 +20,7 @@ const walletManager = new WalletManager({ }, WalletId.KMD, WalletId.KIBISIS, - { - id: WalletId.LIQUID - }, + WalletId.LIQUID, { id: WalletId.LUTE, options: { siteName: 'Example Site' } diff --git a/examples/vue-ts/package.json b/examples/vue-ts/package.json index f711192f..222ae7a1 100644 --- a/examples/vue-ts/package.json +++ b/examples/vue-ts/package.json @@ -9,6 +9,7 @@ "typecheck": "vue-tsc --noEmit" }, "dependencies": { + "@algorandfoundation/liquid-auth-use-wallet-client": "1.1.0", "@blockshake/defly-connect": "^1.1.6", "@perawallet/connect": "^1.3.4", "@txnlab/use-wallet-vue": "workspace:*", diff --git a/examples/vue-ts/src/main.ts b/examples/vue-ts/src/main.ts index 4b09a8db..98295e5c 100644 --- a/examples/vue-ts/src/main.ts +++ b/examples/vue-ts/src/main.ts @@ -20,9 +20,7 @@ app.use(WalletManagerPlugin, { }, WalletId.KMD, WalletId.KIBISIS, - { - id: WalletId.LIQUID - }, + WalletId.LIQUID, { id: WalletId.LUTE, options: { siteName: 'Example Site' } diff --git a/packages/use-wallet/src/__tests__/wallets/liquid.test.ts b/packages/use-wallet/src/__tests__/wallets/liquid.test.ts index 83d37988..ef8ce286 100644 --- a/packages/use-wallet/src/__tests__/wallets/liquid.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/liquid.test.ts @@ -1,11 +1,24 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest' import { Store } from '@tanstack/store' import { Transaction } from 'algosdk' +import { logger } from 'src/logger' import { StorageAdapter } from 'src/storage' -import { LOCAL_STORAGE_KEY, State, defaultState } from 'src/store' +import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { LiquidWallet } from 'src/wallets/liquid' import { WalletId } from 'src/wallets/types' +// Mock logger +vi.mock('src/logger', () => ({ + logger: { + createScopedLogger: vi.fn().mockReturnValue({ + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn() + }) + } +})) + // Mock storage adapter vi.mock('src/storage', () => ({ StorageAdapter: { @@ -33,10 +46,30 @@ vi.mock('@algorandfoundation/liquid-auth-use-wallet-client', () => ({ ICON: 'mockIcon' })) +function createWalletWithStore(store: Store): LiquidWallet { + return new LiquidWallet({ + id: WalletId.LIQUID, + options: { + RTC_config_username: 'username', + RTC_config_credential: 'credential' + }, + metadata: {}, + getAlgodClient: () => ({}) as any, + store, + subscribe: vi.fn() + }) +} + describe('LiquidWallet', () => { let wallet: LiquidWallet let store: Store let mockInitialState: State | null = null + let mockLogger: { + debug: Mock + info: Mock + warn: Mock + error: Mock + } const account1 = { name: 'Liquid Account 1', @@ -59,15 +92,16 @@ describe('LiquidWallet', () => { } }) + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn() + } + vi.mocked(logger.createScopedLogger).mockReturnValue(mockLogger) + store = new Store(defaultState) - wallet = new LiquidWallet({ - id: WalletId.LIQUID, - metadata: { name: 'Liquid' }, // Ensure metadata is correctly initialized - getAlgodClient: {} as any, - store, - subscribe: vi.fn(), - options: { RTC_config_username: 'username', RTC_config_credential: 'credential' } - }) + wallet = createWalletWithStore(store) }) afterEach(async () => { @@ -130,19 +164,38 @@ describe('LiquidWallet', () => { it('disconnect: should throw an error if no auth client is found', async () => { wallet.authClient = null - await expect(wallet.disconnect()).rejects.toThrowError( - 'No auth client found to disconnect from' - ) + await expect(wallet.disconnect()).rejects.toThrowError('No auth client to disconnect') }) }) describe('resumeSession', () => { - it('resumeSession: should call disconnect', async () => { + it('resumeSession: should call disconnect if wallet state exists', async () => { + const walletState: WalletState = { + accounts: [account1], + activeAccount: account1 + } + + store = new Store({ + ...defaultState, + wallets: { + [WalletId.LIQUID]: walletState + } + }) + + wallet = createWalletWithStore(store) + const disconnectSpy = vi.spyOn(wallet, 'disconnect') await wallet.resumeSession() expect(disconnectSpy).toHaveBeenCalled() }) + + it('resumeSession: should not call disconnect if no wallet state exists', async () => { + const disconnectSpy = vi.spyOn(wallet, 'disconnect') + await wallet.resumeSession() + + expect(disconnectSpy).not.toHaveBeenCalled() + }) }) describe('signTransactions', () => { diff --git a/packages/use-wallet/src/wallets/liquid.ts b/packages/use-wallet/src/wallets/liquid.ts index d44126c8..72da210a 100644 --- a/packages/use-wallet/src/wallets/liquid.ts +++ b/packages/use-wallet/src/wallets/liquid.ts @@ -3,11 +3,27 @@ import { WalletAccount, WalletConstructor, WalletId } from './types' import { Store } from '@tanstack/store' import { addWallet, State, WalletState } from 'src/store' import { Transaction } from 'algosdk' -import { LiquidAuthClient, ICON } from '@algorandfoundation/liquid-auth-use-wallet-client' -import type { LiquidOptions } from '@algorandfoundation/liquid-auth-use-wallet-client' +import type { + LiquidAuthClient, + LiquidOptions +} from '@algorandfoundation/liquid-auth-use-wallet-client' export { LiquidOptions } +const ICON = `data:image/svg+xml;base64,${btoa(` + + + + + + + + + + + +`)}` + export class LiquidWallet extends BaseWallet { protected store: Store public authClient: LiquidAuthClient | undefined | null @@ -28,7 +44,6 @@ export class LiquidWallet extends BaseWallet { RTC_config_username: 'username', RTC_config_credential: 'credential' } - this.authClient = new LiquidAuthClient(this.options) } static defaultMetadata = { @@ -36,17 +51,27 @@ export class LiquidWallet extends BaseWallet { icon: ICON } - public async connect(_args?: Record): Promise { - if (!this.authClient) { - this.authClient = new LiquidAuthClient(this.options) - } + private async initializeClient(): Promise { + this.logger.info('Initializing client...') + const { LiquidAuthClient } = await import('@algorandfoundation/liquid-auth-use-wallet-client') + + const client = new LiquidAuthClient(this.options) + this.authClient = client + this.logger.info('Client initialized') + return client + } + + public connect = async (_args?: Record): Promise => { + this.logger.info('Connecting...') + const authClient = this.authClient || (await this.initializeClient()) - await this.authClient.connect() + await authClient.connect() - const sessionData = await this.authClient.checkSession() + const sessionData = await authClient.checkSession() const account = sessionData?.user?.wallet if (!account) { + this.logger.error('No accounts found!') throw new Error('No accounts found!') } @@ -67,34 +92,57 @@ export class LiquidWallet extends BaseWallet { wallet: walletState }) - console.info(`[${this.metadata.name}] ✅ Connected.`, walletState) - this.authClient.hideModal() + this.logger.info('Connected successfully', walletState) + authClient.hideModal() return Promise.resolve(walletAccounts) } - public async disconnect(): Promise { + public disconnect = async (): Promise => { + this.logger.info('Disconnecting...') if (!this.authClient) { - throw new Error('No auth client found to disconnect from') + this.logger.error('No auth client to disconnect') + throw new Error('No auth client to disconnect') } await this.authClient.disconnect() this.onDisconnect() - console.info(`[${this.metadata.name}] ✅ Disconnected.`) + this.logger.info('Disconnected.') this.authClient = null } - public resumeSession(): Promise { - return this.disconnect() - } + public resumeSession = async (): Promise => { + try { + const state = this.store.state + const walletState = state.wallets[this.id] - public async signTransactions( - _txnGroup: T | T[], - _indexesToSign?: number[] - ): Promise<(Uint8Array | null)[]> { - if (!this.activeAddress) { - throw new Error('No active account') + // No session to resume + if (!walletState) { + this.logger.info('No session to resume') + return + } + this.disconnect() + } catch (error) { + this.logger.error('Error resuming session', error) + this.onDisconnect() + throw error } + } - return this.authClient!.signTransactions(_txnGroup, this.activeAddress, _indexesToSign) + public signTransactions = async ( + txnGroup: T | T[], + indexesToSign?: number[] + ): Promise<(Uint8Array | null)[]> => { + try { + if (!this.activeAddress) { + throw new Error('No active account') + } + this.logger.debug('Signing transactions...', { txnGroup, indexesToSign }) + + const authClient = this.authClient || (await this.initializeClient()) + return authClient.signTransactions(txnGroup, this.activeAddress, indexesToSign) + } catch (error) { + this.logger.error('Error signing transactions', error) + throw error + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7bfca39..fae549c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,7 +52,7 @@ importers: version: 2.10.2(@testing-library/jest-dom@6.5.0)(solid-js@1.9.1)(vite@5.4.8(@types/node@20.11.30)(terser@5.31.6)) vitest: specifier: 2.1.2 - version: 2.1.2(@types/node@20.11.30)(jsdom@25.0.1)(terser@5.31.6) + version: 2.1.2(@types/node@20.11.30)(jsdom@25.0.1(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@5.0.10))(terser@5.31.6) vue-demi: specifier: 0.14.10 version: 0.14.10(vue@3.5.11(typescript@5.6.2)) @@ -111,6 +111,9 @@ importers: examples/nuxt: dependencies: + '@algorandfoundation/liquid-auth-use-wallet-client': + specifier: 1.1.0 + version: 1.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@blockshake/defly-connect': specifier: ^1.1.6 version: 1.1.6(algosdk@2.9.0)(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -154,6 +157,9 @@ importers: examples/react-ts: dependencies: + '@algorandfoundation/liquid-auth-use-wallet-client': + specifier: 1.1.0 + version: 1.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@blockshake/defly-connect': specifier: ^1.1.6 version: 1.1.6(algosdk@2.9.0)(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -215,6 +221,9 @@ importers: examples/solid-ts: dependencies: + '@algorandfoundation/liquid-auth-use-wallet-client': + specifier: 1.1.0 + version: 1.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@blockshake/defly-connect': specifier: ^1.1.6 version: 1.1.6(algosdk@2.9.0)(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -252,6 +261,9 @@ importers: examples/vanilla-ts: dependencies: + '@algorandfoundation/liquid-auth-use-wallet-client': + specifier: 1.1.0 + version: 1.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@blockshake/defly-connect': specifier: ^1.1.6 version: 1.1.6(algosdk@2.9.0)(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -286,6 +298,9 @@ importers: examples/vue-ts: dependencies: + '@algorandfoundation/liquid-auth-use-wallet-client': + specifier: 1.1.0 + version: 1.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@blockshake/defly-connect': specifier: ^1.1.6 version: 1.1.6(algosdk@2.9.0)(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -13063,7 +13078,7 @@ snapshots: optionalDependencies: vite: 5.4.8(@types/node@20.11.30)(terser@5.31.6) - vitest@2.1.2(@types/node@20.11.30)(jsdom@25.0.1)(terser@5.31.6): + vitest@2.1.2(@types/node@20.11.30)(jsdom@25.0.1(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@5.0.10))(terser@5.31.6): dependencies: '@vitest/expect': 2.1.2 '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@20.11.30)(terser@5.31.6))