Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions examples/vite-core/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const App = () => {
</header>
<main>
<WalletStatus open={open} checkIsOpen={checkIsOpen} balance={balance} />
<MnemonicManager />
<JoinFederation open={open} checkIsOpen={checkIsOpen} />
<GenerateLightningInvoice />
<RedeemEcash />
Expand Down Expand Up @@ -522,4 +523,200 @@ const SendOnchain = () => {
)
}

const MnemonicManager = () => {
const [mnemonicState, setMnemonicState] = useState<string>('')
const [inputMnemonic, setInputMnemonic] = useState<string>('')
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 {
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 {
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 {
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 (
<div className="section mnemonic-section">
<h3>🔑 Mnemonic Manager</h3>

<div className="mnemonic-buttons">
<button
onClick={() => handleAction('get')}
disabled={isLoading}
className={`btn ${activeAction === 'get' ? 'active' : ''}`}
>
Get
</button>
<button
onClick={() => handleAction('set')}
disabled={isLoading}
className={`btn ${activeAction === 'set' ? 'active' : ''}`}
>
Set
</button>
<button
onClick={() => handleAction('generate')}
disabled={isLoading}
className={`btn ${activeAction === 'generate' ? 'active' : ''}`}
>
Generate
</button>
</div>

{activeAction === 'set' && (
<form onSubmit={handleSetMnemonic} className="mnemonic-form">
<textarea
placeholder="Enter 12 or 24 words separated by spaces"
value={inputMnemonic}
onChange={(e) => setInputMnemonic(e.target.value)}
rows={2}
className="mnemonic-input"
/>
<button
type="submit"
disabled={isLoading || !inputMnemonic.trim()}
className="btn btn-primary"
>
{isLoading ? 'Setting...' : 'Set Mnemonic'}
</button>
</form>
)}

{mnemonicState && (
<div className="mnemonic-display">
<div className="mnemonic-output">
<span className={showMnemonic ? '' : 'blurred'}>
{mnemonicState}
</span>
<div className="mnemonic-actions">
<button
onClick={() => setShowMnemonic(!showMnemonic)}
className="btn btn-small"
title={showMnemonic ? 'Hide mnemonic' : 'Show mnemonic'}
>
{showMnemonic ? '👁️' : '👁️‍🗨️'}
</button>
<button
onClick={copyToClipboard}
className="btn btn-small"
disabled={!showMnemonic}
title="Copy to clipboard"
>
📋
</button>
</div>
</div>
</div>
)}

{message && (
<div className={`message ${message.type}`}>{message.text}</div>
)}
</div>
)
}

export default App
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
url = "github:fedimint/fedimint/v0.8.1";
};
fedimint-wasm = {
url = "github:fedimint/fedimint?rev=ba238118bf5b204bc73c1113b6cadd62bca4e66c";
url = "github:fedimint/fedimint?rev=a88f7f6ceb988ee964bf06900183c3c16f7f4c38";
};
};
outputs =
Expand Down
29 changes: 13 additions & 16 deletions packages/core/src/FedimintWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
WalletService,
} from './services'

const DEFAULT_CLIENT_NAME = 'fm-default' as const
const DEFAULT_CLIENT_NAME = 'dd5135b2-c228-41b7-a4f9-3b6e7afe3088' as const
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

???


export class FedimintWallet {
public balance: BalanceService
Expand Down Expand Up @@ -68,14 +68,16 @@ export class FedimintWallet {
async open(clientName: string = DEFAULT_CLIENT_NAME) {
// TODO: Determine if this should be safe or throw
if (this._isOpen) throw new Error('The FedimintWallet is already open.')
const { success } = await this._client.sendSingleMessage<{
success: boolean
}>('open', { clientName })
if (success) {
this._isOpen = !!success

try {
await this._client.openClient(clientName)
this._isOpen = true
this._resolveOpen()
return true
} catch (e) {
this._client.logger.error('Error opening client', e)
return false
}
return success
}

async joinFederation(
Expand All @@ -88,15 +90,10 @@ export class FedimintWallet {
'The FedimintWallet is already open. You can only call `joinFederation` on closed clients.',
)
try {
const response = await this._client.sendSingleMessage<{
success: boolean
}>('join', { inviteCode, clientName })
if (response.success) {
this._isOpen = true
this._resolveOpen()
}

return response.success
await this._client.joinFederation(inviteCode, clientName)
this._isOpen = true
this._resolveOpen()
return true
} catch (e) {
this._client.logger.error('Error joining federation', e)
return false
Expand Down
79 changes: 49 additions & 30 deletions packages/core/src/WalletDirector.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { TransportClient } from './transport'
import { type LogLevel } from './utils/logger'
import { FederationConfig, JSONValue } from './types'
import { Transport } from '@fedimint/types'
import {
Transport,
ParsedInviteCode,
ParsedBolt11Invoice,
PreviewFederation,
} from '@fedimint/types'
import { FedimintWallet } from './FedimintWallet'

export class WalletDirector {
Expand Down Expand Up @@ -40,15 +45,6 @@ export class WalletDirector {
return new FedimintWallet(this._client)
}

async previewFederation(inviteCode: string) {
await this._client.initialize()
const response = this._client.sendSingleMessage<{
config: FederationConfig
federation_id: string
}>('previewFederation', { inviteCode })
return response
}

/**
* Sets the log level for the library.
* @param level The desired log level ('DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE').
Expand All @@ -65,26 +61,21 @@ export class WalletDirector {
* The response includes the federation_id and url.
*
* @param {string} inviteCode - The invite code to be parsed.
* @returns {Promise<{ federation_id: string, url: string}>}
* @returns {Promise<ParsedInviteCode>}
* A promise that resolves to an object containing:
* - `federation_id`: The id of the feder.
* - `federation_id`: The id of the federation.
* - `url`: One of the apipoints to connect to the federation
*
* @throws {Error} If the TransportClient encounters an issue during the parsing process.
*
* @example
* const inviteCode = "example-invite-code";
* const parsedCode = await wallet.parseInviteCode(inviteCode);
* const parsedCode = await walletDirector.parseInviteCode(inviteCode);
* console.log(parsedCode.federation_id, parsedCode.url);
*/
async parseInviteCode(inviteCode: string) {
async parseInviteCode(inviteCode: string): Promise<ParsedInviteCode> {
await this._client.initialize()
const response = await this._client.sendSingleMessage<{
type: string
data: JSONValue
requestId: number
}>('parseInviteCode', { inviteCode })
return response
return this._client.parseInviteCode(inviteCode)
}

/**
Expand All @@ -94,26 +85,54 @@ export class WalletDirector {
* The response includes details such as the amount, expiry, and memo.
*
* @param {string} invoiceStr - The BOLT11 invoice string to be parsed.
* @returns {Promise<{ amount: string, expiry: number, memo: string }>}
* @returns {Promise<ParsedBolt11Invoice>}
* A promise that resolves to an object containing:
* - `amount`: The amount specified in the invoice.
* - `amount`: The amount specified in the invoice (in satoshis).
* - `expiry`: The expiry time of the invoice in seconds.
* - `memo`: A description or memo attached to the invoice.
*
* @throws {Error} If the TransportClient encounters an issue during the parsing process.
*
* @example
* const invoiceStr = "lnbc1...";
* const parsedInvoice = await wallet.parseBolt11Invoice(invoiceStr);
* const parsedInvoice = await walletDirector.parseBolt11Invoice(invoiceStr);
* console.log(parsedInvoice.amount, parsedInvoice.expiry, parsedInvoice.memo);
*/
async parseBolt11Invoice(invoiceStr: string) {
async parseBolt11Invoice(invoiceStr: string): Promise<ParsedBolt11Invoice> {
await this._client.initialize()
const response = await this._client.sendSingleMessage<{
type: string
data: JSONValue
requestId: number
}>('parseBolt11Invoice', { invoiceStr })
return response
return this._client.parseBolt11Invoice(invoiceStr)
}

/**
* Previews the configuration of a federation using the provided invite code.
*
* Retrieves and returns the federation configuration
* associated with the given invite code.
*
* @param inviteCode - The invite code used to identify the federation to preview.
* @returns {Promise<PreviewFederation>} A promise that resolves to a `PreviewFederation` object containing the federation's
* configuration and ID.
* @example
* const inviteCode = "example-invite-code";
* const federationPreview = await walletDirector.previewFederation(inviteCode);
* console.log(federationPreview.config, federationPreview.federation_id);
*/
async previewFederation(inviteCode: string): Promise<PreviewFederation> {
return await this._client.previewFederation(inviteCode)
}

async generateMnemonic(): Promise<string[]> {
const result = await this._client.generateMnemonic()
return result.mnemonic
}

async getMnemonic(): Promise<string[]> {
const result = await this._client.getMnemonic()
return result.mnemonic
}

async setMnemonic(words: string[]): Promise<boolean> {
const result = await this._client.setMnemonic(words)
return result.success
}
}
Loading
Loading