diff --git a/.prettierignore b/.prettierignore index 1d207ae5..167e4b8f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,4 +10,8 @@ packages/*/dist/** docs/core/FedimintWallet/BalanceService/subscribeBalance.md docs/core/FedimintWallet/LightningService/createInvoice.md docs/core/FedimintWallet/LightningService/payInvoice.md +docs/core/FedimintWallet/RecoveryService/subscribeToRecoveryProgress.md +docs/core/FedimintWallet/RecoveryService/backupToFederation.md +docs/core/FedimintWallet/WalletService/sendOnchain.md +docs/core/FedimintWallet/FederationService/listOperations.md flake.lock diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index 3d8ca2bf..6fbd20c8 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -142,7 +142,15 @@ const FedimintWalletSidebar = [ { text: 'RecoveryService', base: '/core/FedimintWallet/RecoveryService/', - items: [{ text: 'Docs TODO' }], + items: [ + { text: 'backupToFederation()', link: 'backupToFederation' }, + { text: 'hasPendingRecoveries()', link: 'hasPendingRecoveries' }, + { + text: 'subscribeToRecoveryProgress()', + link: 'subscribeToRecoveryProgress', + }, + { text: 'waitForRecovery()', link: 'waitForRecovery' }, + ], }, ], }, diff --git a/docs/core/FedimintWallet/BalanceService/getBalance.md b/docs/core/FedimintWallet/BalanceService/getBalance.md index 99820485..dc8900a8 100644 --- a/docs/core/FedimintWallet/BalanceService/getBalance.md +++ b/docs/core/FedimintWallet/BalanceService/getBalance.md @@ -4,6 +4,8 @@ Get the current balance of the wallet in milli-satoshis (MSats). +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/BalanceService/subscribeBalance.md b/docs/core/FedimintWallet/BalanceService/subscribeBalance.md index c9162d32..ed1678b2 100644 --- a/docs/core/FedimintWallet/BalanceService/subscribeBalance.md +++ b/docs/core/FedimintWallet/BalanceService/subscribeBalance.md @@ -4,6 +4,8 @@ Subscribe to balance updates as they occur. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/FederationService/getConfig.md b/docs/core/FedimintWallet/FederationService/getConfig.md index e1e8eade..c82e7c63 100644 --- a/docs/core/FedimintWallet/FederationService/getConfig.md +++ b/docs/core/FedimintWallet/FederationService/getConfig.md @@ -4,6 +4,8 @@ Access configuration details about a connected federation. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/FederationService/getFederationId.md b/docs/core/FedimintWallet/FederationService/getFederationId.md index b4164295..9c2f73d2 100644 --- a/docs/core/FedimintWallet/FederationService/getFederationId.md +++ b/docs/core/FedimintWallet/FederationService/getFederationId.md @@ -4,6 +4,8 @@ Access the `federationId` of the connected Federation. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/FederationService/getInviteCode.md b/docs/core/FedimintWallet/FederationService/getInviteCode.md index c2952eef..bfa3cd99 100644 --- a/docs/core/FedimintWallet/FederationService/getInviteCode.md +++ b/docs/core/FedimintWallet/FederationService/getInviteCode.md @@ -4,6 +4,8 @@ Access the invite code for the connected federation. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/FederationService/getOperation.md b/docs/core/FedimintWallet/FederationService/getOperation.md index 336fdf73..9230249a 100644 --- a/docs/core/FedimintWallet/FederationService/getOperation.md +++ b/docs/core/FedimintWallet/FederationService/getOperation.md @@ -4,6 +4,8 @@ Get the transaction data using the operation id. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' @@ -14,7 +16,7 @@ const wallet = await director.createWallet() await wallet.open() -const operationId = - '3ff56b29cf014b9ff6c8b6b4aa78e02d3c429de7112bfaf42a876f6a797ddf8b' -const config = await wallet.federation.getOperation(operationId) +const operationId = // [!code focus] + '3ff56b29cf014b9ff6c8b6b4aa78e02d3c429de7112bfaf42a876f6a797ddf8b' // [!code focus] +const config = await wallet.federation.getOperation(operationId) // [!code focus] ``` diff --git a/docs/core/FedimintWallet/FederationService/listOperations.md b/docs/core/FedimintWallet/FederationService/listOperations.md index 5d5b643a..0b1e295c 100644 --- a/docs/core/FedimintWallet/FederationService/listOperations.md +++ b/docs/core/FedimintWallet/FederationService/listOperations.md @@ -4,6 +4,8 @@ Returns a paginated list of operations (transactions) from the federation. In case limit and lastseen not given, returns all the available operations. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' @@ -15,10 +17,10 @@ const wallet = await director.createWallet() await wallet.open() const limit = 10 -const lastseen = { - creation_time: { secs_since_epoch: 2323233, nanos_since_epoch: 93429234 }, - operation_id: - '3ff56b29cf014b9ff6c8b6b4aa78e02d3c429de7112bfaf42a876f6a797ddf8b', -} -const operations = await wallet.federation.listOperations(limit, lastseen) +const lastseen = { // [!code focus] + creation_time: { secs_since_epoch: 2323233, nanos_since_epoch: 93429234 }, // [!code focus] + operation_id: // [!code focus] + '3ff56b29cf014b9ff6c8b6b4aa78e02d3c429de7112bfaf42a876f6a797ddf8b', // [!code focus] +} // [!code focus] +const operations = await wallet.federation.listOperations(limit, lastseen) // [!code focus] ``` diff --git a/docs/core/FedimintWallet/FederationService/listTransactions.md b/docs/core/FedimintWallet/FederationService/listTransactions.md index 4dae4cf5..2473df43 100644 --- a/docs/core/FedimintWallet/FederationService/listTransactions.md +++ b/docs/core/FedimintWallet/FederationService/listTransactions.md @@ -4,6 +4,8 @@ Returns a paginated list of transactions from the federation. In case limit and lastseen not given, returns all the available transactions. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash /** * Represents wallet transaction record. diff --git a/docs/core/FedimintWallet/LightningService/createInvoice.md b/docs/core/FedimintWallet/LightningService/createInvoice.md index 51f47acd..65175881 100644 --- a/docs/core/FedimintWallet/LightningService/createInvoice.md +++ b/docs/core/FedimintWallet/LightningService/createInvoice.md @@ -8,6 +8,8 @@ You can use `subscribeLnReceive` to track the invoice status. `waitForReceive` returns a `Promise` that resolves when the invoice succeeds or `timeoutMs` is reached. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/LightningService/payInvoice.md b/docs/core/FedimintWallet/LightningService/payInvoice.md index 229db9f7..fee528db 100644 --- a/docs/core/FedimintWallet/LightningService/payInvoice.md +++ b/docs/core/FedimintWallet/LightningService/payInvoice.md @@ -4,6 +4,8 @@ Attempts to pay an invoice. Returns a `Promise` that resolves when the payment succeeds or fails / times out. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' @@ -32,6 +34,8 @@ You can use `subscribeLnPay` and `subscribeInternalPay` to track the payment sta `subscribeInternalPay` can be used to track the internal payments status +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/MintService/getNotesByDenomination.md b/docs/core/FedimintWallet/MintService/getNotesByDenomination.md index 3e652d6f..8267c5e8 100644 --- a/docs/core/FedimintWallet/MintService/getNotesByDenomination.md +++ b/docs/core/FedimintWallet/MintService/getNotesByDenomination.md @@ -4,6 +4,8 @@ Gives count of ecash notes by denomination present in the wallet. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' @@ -14,6 +16,6 @@ const wallet = await director.createWallet() await wallet.open() -const notes = await wallet.mint.getNotesByDenomination() +const notes = await wallet.mint.getNotesByDenomination() // [!code focus] console.log('Notes are: ', notes) // {1: 2, 2: 1, 8: 2} ``` diff --git a/docs/core/FedimintWallet/MintService/parseNotes.md b/docs/core/FedimintWallet/MintService/parseNotes.md index 1c1f0246..857e5888 100644 --- a/docs/core/FedimintWallet/MintService/parseNotes.md +++ b/docs/core/FedimintWallet/MintService/parseNotes.md @@ -4,6 +4,8 @@ Parses an ecash note string without redeeming it. Use [`redeemEcash()`](./redeemEcash) to redeem the notes. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/MintService/redeemEcash.md b/docs/core/FedimintWallet/MintService/redeemEcash.md index 17e85048..531154ca 100644 --- a/docs/core/FedimintWallet/MintService/redeemEcash.md +++ b/docs/core/FedimintWallet/MintService/redeemEcash.md @@ -4,6 +4,8 @@ Redeem a set of ecash notes. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/MintService/spendNotes.md b/docs/core/FedimintWallet/MintService/spendNotes.md index 3a96e199..dbda49bb 100644 --- a/docs/core/FedimintWallet/MintService/spendNotes.md +++ b/docs/core/FedimintWallet/MintService/spendNotes.md @@ -4,6 +4,8 @@ Generates ecash notes for spending. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/RecoveryService/backupToFederation.md b/docs/core/FedimintWallet/RecoveryService/backupToFederation.md new file mode 100644 index 00000000..08a5be3a --- /dev/null +++ b/docs/core/FedimintWallet/RecoveryService/backupToFederation.md @@ -0,0 +1,45 @@ +# Backup to Federation + +### `recovery.backupToFederation(metadata?: JSONValue)` + +Backup the wallet state to the federation with optional metadata. You should call this function periodically to refresh the user's backups. + +> **WARNING**: This function will throw an error if the wallet is currently recovering. + +```ts twoslash +// @esModuleInterop +import { WalletDirector } from '@fedimint/core' +import { WasmWorkerTransport } from '@fedimint/transport-web' + +const director = new WalletDirector(new WasmWorkerTransport()) +const wallet = await director.createWallet() + +await wallet.open() + +// Backup without metadata +try { + await wallet.recovery.backupToFederation() // [!code focus] + console.log('Backup successful') +} catch (error) { + console.error('Failed to backup', error) +} + +// Backup with metadata +try { + await wallet.recovery.backupToFederation({ // [!code focus] + timestamp: Date.now(), // [!code focus] + note: 'My backup', // [!code focus] + }) // [!code focus] + console.log('Backup with metadata successful') +} catch (error) { + console.error('Failed to backup', error) +} +``` + +## Parameters + +- `metadata` (optional): A JSON value containing custom metadata to store with the backup. Defaults to an empty object if not provided. + +## Returns + +Returns a `Promise` that resolves when the backup is complete. diff --git a/docs/core/FedimintWallet/RecoveryService/hasPendingRecoveries.md b/docs/core/FedimintWallet/RecoveryService/hasPendingRecoveries.md new file mode 100644 index 00000000..7fc0bb7c --- /dev/null +++ b/docs/core/FedimintWallet/RecoveryService/hasPendingRecoveries.md @@ -0,0 +1,31 @@ +# Has Pending Recoveries + +### `recovery.hasPendingRecoveries()` + +Check if there are any pending recovery operations. + +```ts twoslash +// @esModuleInterop +import { WalletDirector } from '@fedimint/core' +import { WasmWorkerTransport } from '@fedimint/transport-web' + +const director = new WalletDirector(new WasmWorkerTransport()) +const wallet = await director.createWallet() + +await wallet.open() + +try { + const hasPending = await wallet.recovery.hasPendingRecoveries() // [!code focus] + if (hasPending) { + console.log('Recovery operations in progress') + } else { + console.log('No pending recoveries') + } +} catch (error) { + console.error('Failed to check recovery status', error) +} +``` + +## Returns + +Returns a `Promise` that resolves to `true` if there are pending recoveries, `false` otherwise. diff --git a/docs/core/FedimintWallet/RecoveryService/subscribeToRecoveryProgress.md b/docs/core/FedimintWallet/RecoveryService/subscribeToRecoveryProgress.md new file mode 100644 index 00000000..accc566e --- /dev/null +++ b/docs/core/FedimintWallet/RecoveryService/subscribeToRecoveryProgress.md @@ -0,0 +1,40 @@ +# Subscribe to Recovery Progress + +### `recovery.subscribeToRecoveryProgress(onSuccess, onError)` + +Subscribe to recovery progress updates for all modules. + +```ts twoslash +// @esModuleInterop +import { WalletDirector } from '@fedimint/core' +import { WasmWorkerTransport } from '@fedimint/transport-web' + +const director = new WalletDirector(new WasmWorkerTransport()) +const wallet = await director.createWallet() + +await wallet.open() + +const unsubscribe = wallet.recovery.subscribeToRecoveryProgress( // [!code focus] + (progress) => { // [!code focus] + console.log('Module:', progress.module_id) // [!code focus] + console.log('Progress:', progress.progress) // [!code focus] + }, // [!code focus] + (error) => { // [!code focus] + console.error('Recovery error:', error) // [!code focus] + }, // [!code focus] +) // [!code focus] + +// Later, to stop listening for updates +unsubscribe() +``` + +## Parameters + +- `onSuccess`: Callback function invoked with progress updates. Receives an object containing: + - `module_id`: The module identifier (number) + - `progress`: The progress data (JSONValue) +- `onError`: Callback function invoked when an error occurs. Receives an error string. + +## Returns + +Returns a function that can be called to unsubscribe from progress updates. diff --git a/docs/core/FedimintWallet/RecoveryService/waitForRecovery.md b/docs/core/FedimintWallet/RecoveryService/waitForRecovery.md new file mode 100644 index 00000000..8f4e9f98 --- /dev/null +++ b/docs/core/FedimintWallet/RecoveryService/waitForRecovery.md @@ -0,0 +1,28 @@ +# Wait for Recovery + +### `recovery.waitForRecovery()` + +Wait for all pending recovery operations of a particular wallet to complete. + +```ts twoslash +// @esModuleInterop +import { WalletDirector } from '@fedimint/core' +import { WasmWorkerTransport } from '@fedimint/transport-web' + +const director = new WalletDirector(new WasmWorkerTransport()) +const wallet = await director.createWallet() + +await wallet.open() + +try { + console.log('Waiting for recoveries to complete...') + await wallet.recovery.waitForRecovery() // [!code focus] + console.log('All recoveries completed') +} catch (error) { + console.error('Recovery failed', error) +} +``` + +## Returns + +Returns a `Promise` that resolves when all recovery operations are complete. diff --git a/docs/core/FedimintWallet/WalletService/generateAddress.md b/docs/core/FedimintWallet/WalletService/generateAddress.md index 5c6bd835..a9d590c4 100644 --- a/docs/core/FedimintWallet/WalletService/generateAddress.md +++ b/docs/core/FedimintWallet/WalletService/generateAddress.md @@ -4,6 +4,8 @@ Create an onchain address. Returns a `deposit_address` (string) and an `operation_id` (string). +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/WalletService/getWalletSummary.md b/docs/core/FedimintWallet/WalletService/getWalletSummary.md index 4f68a563..044b161d 100644 --- a/docs/core/FedimintWallet/WalletService/getWalletSummary.md +++ b/docs/core/FedimintWallet/WalletService/getWalletSummary.md @@ -4,6 +4,8 @@ Gives wallet UTXO set +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/docs/core/FedimintWallet/WalletService/sendOnchain.md b/docs/core/FedimintWallet/WalletService/sendOnchain.md index b72f171e..69049c27 100644 --- a/docs/core/FedimintWallet/WalletService/sendOnchain.md +++ b/docs/core/FedimintWallet/WalletService/sendOnchain.md @@ -4,6 +4,8 @@ Attempts to send bitcoin to an onchain address. +> **WARNING**: This function will throw an error if the wallet is currently recovering. + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' @@ -17,7 +19,7 @@ await wallet.open() const amount = 20 // amount in Sats // [!code focus] const address = 'bc1q...' // [!code focus] -const { operation_id } = await wallet.wallet.sendOnchain( +const { operation_id } = await wallet.wallet.sendOnchain( // [!code focus] amount, // [!code focus] address, // [!code focus] ) // [!code focus] diff --git a/docs/core/FedimintWallet/joinFederation.md b/docs/core/FedimintWallet/joinFederation.md index ae9a1ddd..961b63c6 100644 --- a/docs/core/FedimintWallet/joinFederation.md +++ b/docs/core/FedimintWallet/joinFederation.md @@ -4,6 +4,10 @@ Attempts to join a federation. +::: info +If there is an existing backup stored with the federation associated with the user's root mnemonic, it will be automatically fetched and restored when you join the federation. +::: + ```ts twoslash // @esModuleInterop import { WalletDirector } from '@fedimint/core' diff --git a/examples/next-js/app/globals.css b/examples/next-js/app/globals.css index f73f4812..e37c6bb4 100644 --- a/examples/next-js/app/globals.css +++ b/examples/next-js/app/globals.css @@ -172,3 +172,63 @@ button[type='submit'] { padding: 0.5rem; border-radius: 4px; } + +.button-group { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.preview-result { + margin-top: 1rem; + padding: 1rem; + background-color: #333; + border-radius: 8px; + border: 1px solid #444; +} + +.preview-result h4 { + margin-top: 0; + margin-bottom: 0.75rem; + color: rgba(255, 255, 255, 0.87); +} + +.preview-info { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.preview-info > div { + line-height: 1.6; +} + +.preview-info strong { + color: #4a9eff; +} + +.preview-result details { + margin-top: 0.75rem; + cursor: pointer; +} + +.preview-result summary { + padding: 0.5rem; + background-color: #242424; + border-radius: 4px; + user-select: none; +} + +.preview-result summary:hover { + background-color: #2a2a2a; +} + +.preview-result pre { + margin-top: 0.5rem; + padding: 0.75rem; + background-color: #1a1a1a; + border-radius: 4px; + overflow-x: auto; + font-size: 0.85rem; + line-height: 1.4; +} diff --git a/examples/next-js/app/page.tsx b/examples/next-js/app/page.tsx index 3c1e1632..04053313 100644 --- a/examples/next-js/app/page.tsx +++ b/examples/next-js/app/page.tsx @@ -1,10 +1,10 @@ 'use client' import { SetStateAction, useCallback, useEffect, useState } from 'react' -import { wallet } from '@/utils/wallet' +import { wallet, director } from '@/utils/wallet' const TESTNET_FEDERATION_CODE = - 'fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er' + 'fed11qgqrgvnhwden5te0v9k8q6rp9ekh2arfdeukuet595cr2ttpd3jhq6rzve6zuer9wchxvetyd938gcewvdhk6tcqqysptkuvknc7erjgf4em3zfh90kffqf9srujn6q53d6r056e4apze5cw27h75' // Expose the wallet to the global window object for testing // @ts-ignore @@ -14,7 +14,7 @@ const useIsOpen = () => { const [open, setIsOpen] = useState(false) const checkIsOpen = useCallback(() => { - if (open !== wallet.isOpen()) { + if (wallet && open !== wallet?.isOpen()) { setIsOpen(wallet.isOpen()) } }, [open]) @@ -30,7 +30,7 @@ const useBalance = (checkIsOpen: () => void) => { const [balance, setBalance] = useState(0) useEffect(() => { - const unsubscribe = wallet.balance.subscribeBalance( + const unsubscribe = wallet?.balance.subscribeBalance( (balance: SetStateAction) => { checkIsOpen() setBalance(balance) @@ -38,7 +38,7 @@ const useBalance = (checkIsOpen: () => void) => { ) return () => { - unsubscribe() + unsubscribe?.() } }, [checkIsOpen]) @@ -78,15 +78,220 @@ const App = () => {
+ + + + + +
) } +const MnemonicManager = () => { + const [mnemonicState, setMnemonicState] = useState('') + const [inputMnemonic, setInputMnemonic] = useState('') + const [activeAction, setActiveAction] = useState< + 'get' | 'set' | 'generate' | null + >(null) + const [isLoading, setIsLoading] = useState(false) + const [message, setMessage] = useState<{ + text: string + type: 'success' | 'error' + }>() + const [showMnemonic, setShowMnemonic] = useState(false) + + const clearMessage = () => setMessage(undefined) + + // Helper function to extract user-friendly error messages + const extractErrorMessage = (error: any): string => { + let errorMsg = 'Operation failed' + + if (error instanceof Error) { + errorMsg = error.message + } else if (typeof error === 'object' && error !== null) { + // Handle RPC error objects + const rpcError = error as any + if (rpcError.error) { + errorMsg = rpcError.error + } else if (rpcError.message) { + errorMsg = rpcError.message + } + } + + return errorMsg + } + + const handleAction = async (action: 'get' | 'set' | 'generate') => { + if (activeAction === action) { + setActiveAction(null) + return + } + setActiveAction(action) + clearMessage() + + if (action === 'get') { + await handleGetMnemonic() + } else if (action === 'generate') { + await handleGenerateMnemonic() + } + } + + const handleGenerateMnemonic = async () => { + setIsLoading(true) + try { + if (!director) throw new Error('Director unavailable') + const newMnemonic = await director.generateMnemonic() + setMnemonicState(newMnemonic.join(' ')) + setMessage({ text: 'New mnemonic generated!', type: 'success' }) + setShowMnemonic(true) + } catch (error) { + console.error('Error generating mnemonic:', error) + const errorMsg = extractErrorMessage(error) + setMessage({ text: errorMsg, type: 'error' }) + } finally { + setIsLoading(false) + } + } + + const handleGetMnemonic = async () => { + setIsLoading(true) + try { + if (!director) throw new Error('Director unavailable') + const mnemonic = await director.getMnemonic() + if (mnemonic && mnemonic.length > 0) { + setMnemonicState(mnemonic.join(' ')) + setMessage({ text: 'Mnemonic retrieved!', type: 'success' }) + setShowMnemonic(true) + } else { + setMessage({ text: 'No mnemonic found', type: 'error' }) + } + } catch (error) { + console.error('Error getting mnemonic:', error) + const errorMsg = extractErrorMessage(error) + setMessage({ text: errorMsg, type: 'error' }) + } finally { + setIsLoading(false) + } + } + + const handleSetMnemonic = async (e: React.FormEvent) => { + e.preventDefault() + if (!inputMnemonic.trim()) return + + setIsLoading(true) + try { + if (!director) throw new Error('Director unavailable') + const words = inputMnemonic.trim().split(/\s+/) + await director.setMnemonic(words) + setMessage({ text: 'Mnemonic set successfully!', type: 'success' }) + setInputMnemonic('') + setMnemonicState(words.join(' ')) + setActiveAction(null) + } catch (error) { + console.error('Error setting mnemonic:', error) + const errorMsg = extractErrorMessage(error) + setMessage({ text: errorMsg, type: 'error' }) + } finally { + setIsLoading(false) + } + } + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(mnemonicState) + setMessage({ text: 'Copied to clipboard!', type: 'success' }) + } catch (error) { + setMessage({ text: 'Failed to copy', type: 'error' }) + } + } + + return ( +
+

🔑 Mnemonic Manager

+ +
+ + + +
+ + {activeAction === 'set' && ( +
+