diff --git a/apps/api/src/dynamic-bridge-discovery/bridge.loader.ts b/apps/api/src/dynamic-bridge-discovery/bridge.loader.ts index ce6e340..0c7e0e1 100644 --- a/apps/api/src/dynamic-bridge-discovery/bridge.loader.ts +++ b/apps/api/src/dynamic-bridge-discovery/bridge.loader.ts @@ -1,19 +1,6 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import * as fs from 'fs'; import * as path from 'path'; -<<<<<<< HEAD:src/dynamic-bridge-discovery/bridge.loader.ts -import { - BridgeAdapter, - BridgeAdapterConstructor, -} from '../interfaces/bridge-adapter.interface'; -import { BridgeModuleConfig } from '../interfaces/bridge-config.interface'; -import { - BridgeInitializationException, - BridgeLoadException, -} from '../exceptions/bridge.exceptions'; -import { BridgeRegistry } from '../registry/bridge.registry'; -import { BRIDGE_ADAPTER_METADATA } from '../decorators/bridge.decorators'; -======= import { BridgeRegistry } from './bridge.registry'; import { BridgeModuleConfig } from './bridge-config.interface'; import { @@ -25,7 +12,6 @@ import { BridgeLoadException, } from './bridge.exceptions'; import { BRIDGE_ADAPTER_METADATA } from './bridge.decorators'; ->>>>>>> 902330b94c4294029cf45eb84c6121443fbb0427:apps/api/src/dynamic-bridge-discovery/bridge.loader.ts @Injectable() export class BridgeLoader implements OnModuleInit { @@ -141,25 +127,12 @@ export class BridgeLoader implements OnModuleInit { } <<<<<<< HEAD:src/dynamic-bridge-discovery/bridge.loader.ts - const mergedConfig = { - ...(this.config.globalConfig ?? {}), - ...(adapterConfig.options ?? {}), - }; - const instance: BridgeAdapter = new AdapterClass(mergedConfig); - - await this.initializeAdapter(instance); - this.registry.register(instance, { - source: resolvedPath, - configKey: name, - }); -======= await this.createAndRegisterAdapter( AdapterClass, resolvedPath, adapterConfig.options ?? {}, name, ); ->>>>>>> 902330b94c4294029cf45eb84c6121443fbb0427:apps/api/src/dynamic-bridge-discovery/bridge.loader.ts } catch (err) { throw new BridgeLoadException(resolvedPath, err as Error); } diff --git a/apps/api/src/dynamic-bridge-discovery/bridge.registry.ts b/apps/api/src/dynamic-bridge-discovery/bridge.registry.ts index cadea6f..8afdc87 100644 --- a/apps/api/src/dynamic-bridge-discovery/bridge.registry.ts +++ b/apps/api/src/dynamic-bridge-discovery/bridge.registry.ts @@ -1,12 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -<<<<<<< HEAD:src/dynamic-bridge-discovery/bridge.registry.ts -import { - BridgeAdapter, - BridgeCapability, -} from '../interfaces/bridge-adapter.interface'; -======= import { BridgeAdapter, BridgeCapability } from './bridge-adapter.interface'; ->>>>>>> 902330b94c4294029cf45eb84c6121443fbb0427:apps/api/src/dynamic-bridge-discovery/bridge.registry.ts import { BridgeCapabilityNotFoundException, BridgeDuplicateException, diff --git a/apps/api/src/dynamic-bridge-discovery/demo-bridge.adapter.ts b/apps/api/src/dynamic-bridge-discovery/demo-bridge.adapter.ts new file mode 100644 index 0000000..28c8a50 --- /dev/null +++ b/apps/api/src/dynamic-bridge-discovery/demo-bridge.adapter.ts @@ -0,0 +1,37 @@ +import { BridgeAdapter, BridgeCapability } from './bridge-adapter.interface'; +import { BridgePlugin } from './bridge.decorators'; + +@BridgePlugin({ name: 'demo-bridge', version: '0.1.0' }) +export class DemoBridgeAdapter implements BridgeAdapter { + readonly name = 'demo-bridge'; + readonly version = '0.1.0'; + readonly capabilities: BridgeCapability[] = [ + { name: 'demo', version: '1.0.0', description: 'Demo bridge for plugin architecture' }, + ]; + + private initialized = false; + + async initialize(config?: Record): Promise { + this.initialized = true; + } + + async isHealthy(): Promise { + return this.initialized; + } + + async shutdown(): Promise { + this.initialized = false; + } + + async execute(operation: string, payload: T): Promise { + if (!this.initialized) { + throw new Error('DemoBridgeAdapter not initialized'); + } + + return { + operation, + payload, + handledBy: this.name, + } as unknown as R; + } +} diff --git a/apps/api/src/dynamic-bridge-discovery/http-bridge.adapter.ts b/apps/api/src/dynamic-bridge-discovery/http-bridge.adapter.ts index 001d6dd..08d513f 100644 --- a/apps/api/src/dynamic-bridge-discovery/http-bridge.adapter.ts +++ b/apps/api/src/dynamic-bridge-discovery/http-bridge.adapter.ts @@ -1,13 +1,5 @@ -<<<<<<< HEAD:src/dynamic-bridge-discovery/http-bridge.adapter.ts -import { - BridgeAdapter, - BridgeCapability, -} from '../interfaces/bridge-adapter.interface'; -import { BridgePlugin } from '../decorators/bridge.decorators'; -======= import { BridgeAdapter, BridgeCapability } from './bridge-adapter.interface'; import { BridgePlugin } from './bridge.decorators'; ->>>>>>> 902330b94c4294029cf45eb84c6121443fbb0427:apps/api/src/dynamic-bridge-discovery/http-bridge.adapter.ts @BridgePlugin({ name: 'http-bridge', version: '1.0.0' }) export class HttpBridgeAdapter implements BridgeAdapter { diff --git a/apps/api/src/dynamic-bridge-discovery/index.ts b/apps/api/src/dynamic-bridge-discovery/index.ts index ebb4e55..2270d6c 100644 --- a/apps/api/src/dynamic-bridge-discovery/index.ts +++ b/apps/api/src/dynamic-bridge-discovery/index.ts @@ -29,3 +29,4 @@ export { // Example adapters (not for production use — illustrative only) export { HttpBridgeAdapter } from './http-bridge.adapter'; export { WebSocketBridgeAdapter } from './websocket-bridge.adapter'; +export { DemoBridgeAdapter } from './demo-bridge.adapter'; diff --git a/apps/api/src/dynamic-bridge-discovery/websocket-bridge.adapter.ts b/apps/api/src/dynamic-bridge-discovery/websocket-bridge.adapter.ts index 33bc455..0b0e247 100644 --- a/apps/api/src/dynamic-bridge-discovery/websocket-bridge.adapter.ts +++ b/apps/api/src/dynamic-bridge-discovery/websocket-bridge.adapter.ts @@ -1,13 +1,5 @@ -<<<<<<< HEAD:src/dynamic-bridge-discovery/websocket-bridge.adapter.ts -import { - BridgeAdapter, - BridgeCapability, -} from '../interfaces/bridge-adapter.interface'; -import { BridgePlugin } from '../decorators/bridge.decorators'; -======= import { BridgeAdapter, BridgeCapability } from './bridge-adapter.interface'; import { BridgePlugin } from './bridge.decorators'; ->>>>>>> 902330b94c4294029cf45eb84c6121443fbb0427:apps/api/src/dynamic-bridge-discovery/websocket-bridge.adapter.ts @BridgePlugin({ name: 'ws-bridge', version: '1.0.0' }) export class WebSocketBridgeAdapter implements BridgeAdapter { diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index ab85393..8dc7480 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -6,6 +6,7 @@ import { TransactionProvider, } from '@bridgewise/ui-components'; import { OfflineBanner } from '../components/OfflineBanner'; +import { I18nProvider } from '../components/I18nProvider'; import './globals.css'; const customTheme = { @@ -47,12 +48,14 @@ export default function RootLayout({ - - - - {children} - - + + + + + {children} + + + ); diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index e5f5d97..65d1828 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,7 +1,8 @@ 'use client'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { BridgeWiseProvider, TransactionHeartbeat, @@ -9,6 +10,7 @@ import { BridgeStatus, } from '@bridgewise/ui-components'; import VersionDisplay from '../components/VersionDisplay'; +import { LanguageSwitcher } from '../components/LanguageSwitcher'; const customTheme = { primaryColor: '#22c55e', @@ -21,7 +23,9 @@ const customTheme = { }; function TransactionDemo() { - const { state, updateState, startTransaction, clearState } = useTransaction(); + const { t } = useTranslation(); + const { state, updateState, startTransaction, clearState, executeBatch } = useTransaction(); + const [batchResult, setBatchResult] = useState<{ id: string; txHash: string; success: boolean; error?: string; }[] | null>(null); // Simulate progress useEffect(() => { @@ -37,28 +41,27 @@ function TransactionDemo() { let nextProgress = state.progress + 5; let nextStep = state.step; - if (nextProgress > 20 && nextProgress < 40) nextStep = 'Confirming on source chain...'; - if (nextProgress > 50 && nextProgress < 70) nextStep = 'Bridging assets...'; - if (nextProgress > 80) nextStep = 'Finalizing on destination...'; + if (nextProgress > 20 && nextProgress < 40) nextStep = t('app.statusConfirming'); + if (nextProgress > 50 && nextProgress < 70) nextStep = t('app.statusBridging'); + if (nextProgress > 80) nextStep = t('app.statusFinalizing'); updateState({ progress: Math.min(nextProgress, 100), step: nextStep }); }, 800); return () => clearInterval(interval); - }, [state, updateState]); + }, [state, updateState, t]); return (
+

- BridgeWise Theming Demo + {t('app.title')}

- This page demonstrates the BridgeWise theme system. The heartbeat and status components - are styled via CSS variables injected by BridgeWiseProvider, with a custom - primary color and dark background. + {t('app.description')}

@@ -66,23 +69,89 @@ function TransactionDemo() { onClick={() => startTransaction('tx-' + Date.now())} className="px-6 py-3 rounded-lg text-sm font-medium text-white bg-emerald-500 hover:bg-emerald-600 active:scale-95 transition" > - Start Transaction + {t('app.startTransaction')} +
+ {batchResult && ( +
+

Batch result

+
    + {batchResult.map((item) => ( +
  • + {item.id} + + {item.success ? '✅ success' : '❌ failed'} + {item.error ? `: ${item.error}` : ''} + +
  • + ))} +
+
+ )} +

- Inline BridgeStatus + {t('app.inlineStatusTitle')}

- An inline status card using the same theme variables as the floating heartbeat. + {t('app.inlineStatusText')}

- Component-level Overrides + {t('app.componentOverridesTitle')}

- The floating heartbeat below uses a custom className to adjust its - position while still inheriting all theme variables. + {t('app.componentOverridesText')}

- Trigger a transaction and you'll see the heartbeat appear in the bottom-left corner. + {t('app.componentOverridesHint')}

diff --git a/apps/web/components/I18nProvider.tsx b/apps/web/components/I18nProvider.tsx new file mode 100644 index 0000000..9a90e38 --- /dev/null +++ b/apps/web/components/I18nProvider.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { ReactNode, useEffect } from 'react'; +import { I18nextProvider } from 'react-i18next'; +import i18n from '../i18n'; + +export function I18nProvider({ children }: { children: ReactNode }) { + useEffect(() => { + const storedLanguage = window.localStorage.getItem('bridgewise-language'); + if (storedLanguage && storedLanguage !== i18n.language) { + i18n.changeLanguage(storedLanguage); + } + }, []); + + return {children}; +} diff --git a/apps/web/components/LanguageSwitcher.tsx b/apps/web/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..5471602 --- /dev/null +++ b/apps/web/components/LanguageSwitcher.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { ChangeEvent, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +const AVAILABLE_LANGUAGES = [ + { code: 'en', label: 'English' }, + { code: 'fr', label: 'Français' }, +]; + +export function LanguageSwitcher() { + const { i18n, t } = useTranslation(); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + const stored = window.localStorage.getItem('bridgewise-language'); + if (stored && stored !== i18n.language) { + i18n.changeLanguage(stored); + } + setLoaded(true); + }, [i18n]); + + const handleChange = (event: ChangeEvent) => { + const nextLng = event.target.value; + i18n.changeLanguage(nextLng); + window.localStorage.setItem('bridgewise-language', nextLng); + }; + + if (!loaded) return null; + + return ( +
+ + +
+ ); +} diff --git a/apps/web/components/OfflineBanner.tsx b/apps/web/components/OfflineBanner.tsx index 8099d65..5b07be6 100644 --- a/apps/web/components/OfflineBanner.tsx +++ b/apps/web/components/OfflineBanner.tsx @@ -1,9 +1,11 @@ 'use client'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useOfflineDetection } from '../hooks/useOfflineDetection'; export function OfflineBanner() { + const { t } = useTranslation(); const { isOffline, cache } = useOfflineDetection(); if (!isOffline) return null; @@ -33,8 +35,9 @@ export function OfflineBanner() { /> - You are offline. Showing{' '} - {cachedAt ? `cached data from ${cachedAt}` : 'limited functionality'}. + {t('app.offline', { + cached: cachedAt ? t('app.offlineCached', { when: cachedAt }) : t('app.offlineLimited'), + })}
); diff --git a/apps/web/components/VersionDisplay.tsx b/apps/web/components/VersionDisplay.tsx index af8d50b..0b4a433 100644 --- a/apps/web/components/VersionDisplay.tsx +++ b/apps/web/components/VersionDisplay.tsx @@ -1,6 +1,7 @@ 'use client'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useVersion, VersionData } from '../hooks/useVersion'; export interface VersionDisplayProps { @@ -29,6 +30,7 @@ export const VersionDisplay: React.FC = ({ apiUrl, onClick, }) => { + const { t } = useTranslation(); const { version, loading, error } = useVersion({ apiUrl, enableLogging, @@ -64,7 +66,7 @@ export const VersionDisplay: React.FC = ({ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /> - Loading... + {t('app.loading')} ); } diff --git a/apps/web/i18n.ts b/apps/web/i18n.ts new file mode 100644 index 0000000..b6d6f11 --- /dev/null +++ b/apps/web/i18n.ts @@ -0,0 +1,37 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import en from './locales/en.json'; +import fr from './locales/fr.json'; + +const resources = { + en: { + translation: en, + }, + fr: { + translation: fr, + }, +}; + +if (!i18n.isInitialized) { + i18n + .use(initReactI18next) + .init({ + resources, + lng: 'en', + fallbackLng: 'en', + supportedLngs: ['en', 'fr'], + interpolation: { + escapeValue: false, + }, + react: { + useSuspense: false, + }, + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error('i18next init failed', error); + }); +} + +export default i18n; diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json new file mode 100644 index 0000000..b6e8b13 --- /dev/null +++ b/apps/web/locales/en.json @@ -0,0 +1,21 @@ +{ + "app": { + "title": "BridgeWise Theming Demo", + "description": "This page demonstrates the BridgeWise theme system. The heartbeat and status components are styled via CSS variables injected by BridgeWiseProvider, with a custom primary color and dark background.", + "startTransaction": "Start Transaction", + "clearState": "Clear State", + "inlineStatusTitle": "Inline BridgeStatus", + "inlineStatusText": "An inline status card using the same theme variables as the floating heartbeat.", + "componentOverridesTitle": "Component-level Overrides", + "componentOverridesText": "The floating heartbeat below uses a custom className to adjust its position while still inheriting all theme variables.", + "componentOverridesHint": "Trigger a transaction and you'll see the heartbeat appear in the bottom-left corner.", + "statusConfirming": "Confirming on source chain...", + "statusBridging": "Bridging assets...", + "statusFinalizing": "Finalizing on destination...", + "offline": "You are offline. Showing {{cached}}.", + "offlineCached": "cached data from {{when}}", + "offlineLimited": "limited functionality", + "language": "Language", + "loading": "Loading..." + } +} diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json new file mode 100644 index 0000000..cdc032a --- /dev/null +++ b/apps/web/locales/fr.json @@ -0,0 +1,21 @@ +{ + "app": { + "title": "Démo de thème BridgeWise", + "description": "Cette page démontre le système de thème BridgeWise. Les composants heartbeat et status sont stylés via des variables CSS injectées par BridgeWiseProvider, avec une couleur principale personnalisée et un fond sombre.", + "startTransaction": "Démarrer la transaction", + "clearState": "Réinitialiser l'état", + "inlineStatusTitle": "BridgeStatus en ligne", + "inlineStatusText": "Une carte d'état en ligne utilisant les mêmes variables de thème que le heartbeat flottant.", + "componentOverridesTitle": "Remplacements au niveau du composant", + "componentOverridesText": "Le heartbeat flottant ci-dessous utilise un className personnalisé pour ajuster sa position tout en héritant de toutes les variables de thème.", + "componentOverridesHint": "Déclenchez une transaction et vous verrez le heartbeat apparaître en bas à gauche.", + "statusConfirming": "Confirmation sur la chaîne source...", + "statusBridging": "Transfert d'actifs...", + "statusFinalizing": "Finalisation sur la chaîne de destination...", + "offline": "Vous êtes hors ligne. Affichage de {{cached}}.", + "offlineCached": "données mises en cache depuis {{when}}", + "offlineLimited": "fonctionnalité limitée", + "language": "Langue", + "loading": "Chargement..." + } +} diff --git a/docs/BRIDGE_PLUGIN_SYSTEM.md b/docs/BRIDGE_PLUGIN_SYSTEM.md new file mode 100644 index 0000000..7630636 --- /dev/null +++ b/docs/BRIDGE_PLUGIN_SYSTEM.md @@ -0,0 +1,47 @@ +# Bridge Plugin System + +This project supports dynamic registration of bridge adapters using the `BridgeModule` API and the `BridgePlugin` decorator. + +## Plugin interface + +- `BridgeAdapter` (interface): defines `name`, `version`, `capabilities`, `initialize`, `isHealthy`, `shutdown`, `execute`. +- `BridgePlugin` (decorator): annotate classes for auto-discovery and metadata. + +## Core classes + +- `BridgeRegistry` (singleton): manages adapters by name +- `BridgeLoader` (OnModuleInit): loads adapters from filesystem or config +- `BridgeService`: executes operations via registered bridges and runtime plugin registration + +## Usage + +1. Create a plugin adapter: + +```ts +import { BridgeAdapter, BridgeCapability } from '@bridgewise/api/dynamic-bridge-discovery'; +import { BridgePlugin } from '@bridgewise/api/dynamic-bridge-discovery'; + +@BridgePlugin({ name: 'my-bridge', version: '1.0.0' }) +export class MyBridgeAdapter implements BridgeAdapter { ... } +``` + +2. Register via runtime injection: + +```ts +bridgeService.registerBridge(new MyBridgeAdapter()); +``` + +3. Or set up auto-discovery by config: + +```ts +BridgeModule.forRoot({ + autoDiscover: true, + bridgesDirectory: './plugins', +}); +``` + +## Acceptance criteria + +- plugin interface implemented +- dynamic bridge registration without core code change +- runtime and config-driven plugin loading diff --git a/packages/ui/src/components/TransactionHeartbeat/TransactionContext.tsx b/packages/ui/src/components/TransactionHeartbeat/TransactionContext.tsx index 0741e46..76a18f0 100644 --- a/packages/ui/src/components/TransactionHeartbeat/TransactionContext.tsx +++ b/packages/ui/src/components/TransactionHeartbeat/TransactionContext.tsx @@ -38,6 +38,7 @@ interface TransactionContextType { clearState: () => void; startTransaction: (id: string, initialState?: Partial) => void; recordBridgeTransaction: (transaction: Partial) => Promise; + executeBatch: (items: BatchTransactionInput[]) => Promise; } const TransactionContext = createContext(undefined); @@ -79,6 +80,21 @@ const normalizeBridgeTransaction = ( }; }; +export interface BatchTransactionInput extends Partial { + id: string; +} + +export interface BatchExecutionResultItem { + id: string; + txHash: string; + success: boolean; + error?: string; +} + +export interface BatchExecutionResult { + results: BatchExecutionResultItem[]; +} + export interface TransactionProviderProps { children: ReactNode; historyConfig?: TransactionHistoryConfig; @@ -179,6 +195,96 @@ export const TransactionProvider = ({ }); }, []); + const executeBatch = useCallback( + async (items: BatchTransactionInput[]): Promise => { + const results: BatchExecutionResultItem[] = []; + const total = items.length; + + for (let index = 0; index < items.length; index += 1) { + const item = items[index]; + const progress = Math.round(((index + 1) / total) * 100); + + startTransaction(item.id, { + txHash: item.txHash, + bridgeName: item.bridgeName, + sourceChain: item.sourceChain, + destinationChain: item.destinationChain, + sourceToken: item.sourceToken, + destinationToken: item.destinationToken, + amount: item.amount, + fee: item.fee, + slippagePercent: item.slippagePercent, + account: item.account, + status: 'pending', + step: `Processing batch item ${index + 1}/${total}`, + progress, + }); + + try { + // Simulate delay for transaction processing; replace with real API call if available + await new Promise((resolve) => setTimeout(resolve, 250)); + + const normalized = normalizeBridgeTransaction({ + ...item, + status: 'confirmed', + timestamp: new Date(), + }); + + await historyStorage.upsertTransaction(normalized); + onTransactionTracked?.(normalized); + + results.push({ + id: item.id, + txHash: normalized.txHash, + success: true, + }); + + updateState({ + status: 'success', + step: `Batch item ${index + 1}/${total} confirmed`, + progress, + txHash: normalized.txHash, + }); + } catch (error) { + const errMessage = error instanceof Error ? error.message : 'Unknown error'; + + const failedRecord = normalizeBridgeTransaction({ + ...item, + status: 'failed', + timestamp: new Date(), + }); + + await historyStorage.upsertTransaction(failedRecord); + onTransactionTracked?.(failedRecord); + + results.push({ + id: item.id, + txHash: failedRecord.txHash, + success: false, + error: errMessage, + }); + + updateState({ + status: 'failed', + step: `Batch item ${index + 1}/${total} failed: ${errMessage}`, + progress, + txHash: failedRecord.txHash, + }); + } + } + + setState((prev) => ({ + ...prev, + status: results.every((x) => x.success) ? 'success' : 'failed', + step: `Batch completed: ${results.filter((x) => x.success).length}/${total} succeeded`, + progress: 100, + })); + + return { results }; + }, + [historyStorage, onTransactionTracked, startTransaction, updateState], + ); + const recordBridgeTransaction = useCallback( async (transaction: Partial) => { const normalized = normalizeBridgeTransaction(transaction); @@ -190,7 +296,14 @@ export const TransactionProvider = ({ return ( {children} diff --git a/packages/ui/src/wallet/WalletConnector.tsx b/packages/ui/src/wallet/WalletConnector.tsx index e56008b..713bbc8 100644 --- a/packages/ui/src/wallet/WalletConnector.tsx +++ b/packages/ui/src/wallet/WalletConnector.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { QRCodeCanvas } from 'qrcode.react'; import { useMultiWalletContext } from './MultiWalletProvider'; export const WalletConnector: React.FC = () => { @@ -41,7 +42,17 @@ export const WalletConnector: React.FC = () => { Connect {wallet.name} ))} -
Active Account: {activeAccount ? activeAccount.address : 'None'}
+
+ Active Account: {activeAccount ? activeAccount.address : 'None'} +
+ + {activeAccount && ( +
+ Scan wallet address QR + +
+ )} + {error &&
Error: {error.message}
} );