Technical documentation for developers contributing to BuhoGO.
Back to README | For users: User Guide
- Architecture Overview
- Project Structure
- Wallet Provider System
- L1 Bitcoin Integration
- State Management
- Key Components
- Adding Features
- Styling Guidelines
- Testing
BuhoGO is built with Vue.js 3 and the Quasar Framework. The application follows a provider pattern for wallet operations, allowing different wallet types (Spark, NWC, LNBits) to implement a common interface.
| Layer | Technology | Purpose |
|---|---|---|
| UI Framework | Vue.js 3 | Component-based UI |
| Component Library | Quasar | Pre-built components, theming |
| State Management | Pinia | Centralized reactive state |
| Routing | Vue Router | SPA navigation |
| Build | Vite | Fast development and builds |
| Lightning (Spark) | @buildonspark/spark-sdk | Self-custodial wallet |
| Lightning (NWC) | @getalby/sdk | Wallet connect protocol |
| Lightning (LNBits) | LNBits REST API | Direct API integration |
User Action
|
v
Vue Component
|
v
Pinia Store (wallet.js, addressBook.js)
|
v
Wallet Provider (SparkWalletProvider, NWCWalletProvider, LNBitsWalletProvider)
|
v
External SDK/API (@buildonspark/spark-sdk, @getalby/sdk, or LNBits REST API)
src/
components/
AddressBook/ # Contact management components
AddressBookEntry.vue
AddressBookList.vue
AddressBookModal.vue
L1BitcoinReceive.vue # On-chain Bitcoin deposit UI
L1BitcoinWithdraw.vue # On-chain Bitcoin withdrawal UI
MnemonicDisplay.vue # Seed phrase display
MnemonicVerify.vue # Seed phrase verification
PaymentModal.vue # Payment confirmation dialog
PinEntryDialog.vue # PIN input component
ReceiveModal.vue # Invoice/address generation
SendModal.vue # Payment input and scanning
WalletSwitcher.vue # Wallet selection dropdown
pages/
IndexPage.vue # NWC connection page
Settings.vue # App settings
SparkRestorePage.vue # Seed phrase restore flow
SparkSetupPage.vue # New Spark wallet creation
Wallet.vue # Main wallet dashboard
WelcomePage.vue # Initial wallet type selection
providers/
WalletProvider.js # Abstract base class
SparkWalletProvider.js # Spark wallet implementation
NWCWalletProvider.js # NWC wallet implementation
LNBitsWalletProvider.js # LNBits wallet implementation
WalletFactory.js # Provider creation utilities
stores/
wallet.js # Wallet state and operations
addressBook.js # Contact management
css/
app.css # Global styles and theming
i18n/ # Internationalization files
router/ # Route definitions
The provider pattern abstracts wallet operations so components do not need to know which wallet type is active.
WalletProvider.js defines the interface all providers must implement:
class WalletProvider {
// Identity
getType() // Returns 'spark', 'nwc', or 'lnbits'
isSpark() // Boolean check
// Connection
connect() // Establish connection
disconnect() // Clean up resources
// Balance and Info
getBalance() // Returns { balance, pending, tokenBalances }
getInfo() // Returns wallet metadata
// Payments
createInvoice({ amount, description, expiry })
payInvoice({ invoice, maxFee })
lookupInvoice(paymentHash)
getTransactions({ limit, offset })
// Spark-specific (no-op in NWC)
getSparkAddress()
transferToSparkAddress(sparkAddress, amount)
payLightningAddress(lightningAddress, amountSats, comment)
// L1 Bitcoin (Spark-only)
getL1DepositAddress() // Get Bitcoin deposit address
getPendingDeposits() // Check for incoming deposits
getClaimFeeQuote(txId, outputIndex) // Get claim fee quote
claimDeposit(txId, quoteData, outputIndex) // Claim confirmed deposit
refundDeposit(txId, outputIndex) // Return deposit to sender
getWithdrawalFeeQuote(amountSats, address) // Get withdrawal fees
withdrawToL1({ amountSats, destinationAddress, speed }) // Withdraw to L1
getWithdrawalStatus(requestId) // Check withdrawal status
}Implements the full interface using @buildonspark/spark-sdk:
// Initialization requires mnemonic (after PIN decryption)
await provider.initializeWithMnemonic(mnemonic)
// Zero-fee Spark transfer
await provider.transferToSparkAddress('sp1...', 10000)
// Pay Lightning address (fetches invoice via LNURL)
await provider.payLightningAddress('user@domain.com', 5000, 'Payment note')
// L1 Bitcoin Operations
const address = await provider.getL1DepositAddress() // Returns bc1p...
const deposits = await provider.getPendingDeposits() // Check for incoming deposits
await provider.claimDeposit(txId, quoteData) // Claim a confirmed deposit
await provider.withdrawToL1({ amountSats, destinationAddress, speed }) // Withdraw to L1Wraps @getalby/sdk NostrWebLNProvider:
// Connection uses NWC URL
const provider = new NWCWalletProvider(walletId, {
nwcUrl: 'nostr+walletconnect://...'
})
await provider.connect()
// Standard operations work identically
const balance = await provider.getBalance()
await provider.payInvoice({ invoice: 'lnbc...' })Connects directly to LNBits via REST API:
// Connection uses server URL and admin key
const provider = new LNBitsWalletProvider(walletId, {
serverUrl: 'https://demo.lnbits.com',
walletId: 'abc123...', // LNBits internal wallet ID
adminKey: 'xyz789...' // Admin API key for full access
})
await provider.connect()
// Standard operations
const balance = await provider.getBalance() // Returns balance in sats
await provider.payInvoice({ invoice: 'lnbc...' })
const invoice = await provider.createInvoice({ amount: 1000, description: 'Test' })
// Decode invoice (LNBits-specific)
const decoded = await provider.decodeInvoice('lnbc...')API Endpoints Used:
GET /api/v1/wallet- Get wallet info and balancePOST /api/v1/payments- Create invoice or pay invoiceGET /api/v1/payments/{hash}- Check payment statusGET /api/v1/payments- List transactionsPOST /api/v1/payments/decode- Decode invoice
Authentication:
All requests include the X-Api-Key header with the Admin API key.
Static Validation Method:
// Validate credentials before adding wallet
const result = await LNBitsWalletProvider.validateCredentials(
serverUrl,
walletId,
adminKey
)
// Returns: { valid: true, serverUrl, walletInfo: { name, balance, id } }Utility functions for provider creation and payment parsing:
import { createWalletProvider, parsePaymentDestination } from '@/providers/WalletFactory'
// Create appropriate provider based on wallet data
const provider = createWalletProvider(walletId, walletData)
// Parse any payment format
const result = parsePaymentDestination('lnbc10u1...')
// Returns: { type: 'bolt11', invoice: '...', amount: 1000 }
const result = parsePaymentDestination('sp1qw3...')
// Returns: { type: 'spark_address', address: 'sp1qw3...' }Spark wallets support on-chain Bitcoin (Layer 1) deposits and withdrawals. This section covers the technical implementation.
On-Chain Bitcoin
|
v
Bitcoin Network (mempool.space API for monitoring)
|
v
Spark SDK (deposit address, claim, withdraw)
|
v
L1BitcoinReceive.vue / L1BitcoinWithdraw.vue
|
v
Spark Balance (available for Lightning)
Defined in SparkWalletProvider.js:
const BITCOIN_L1 = {
REQUIRED_CONFIRMATIONS: 3, // Confirmations before claim
MIN_DEPOSIT_SATS: 500, // Minimum deposit amount
DEFAULT_MEMPOOL_API: 'https://mempool.space/api',
TYPICAL_TX_VBYTES: 140 // For fee estimation
}-
Get Deposit Address
const address = await provider.getL1DepositAddress() // Returns: bc1p... (P2TR/Taproot address) // Address is static and reusable
-
Monitor for Deposits
const deposits = await provider.getPendingDeposits() // Returns: [{ txId, outputIndex, amount, confirmations, confirmed }] // Uses mempool.space API to check address UTXOs
-
Get Claim Fee Quote
const quote = await provider.getClaimFeeQuote(txId, outputIndex) // Returns: { creditAmountSats, signature, ... } // creditAmountSats = deposit amount - network fee
-
Claim Deposit
const result = await provider.claimDeposit(txId, quoteData, outputIndex) // Deposit claimed, balance updated
-
Refund Option
await provider.refundDeposit(txId, outputIndex) // Returns deposit to original sender
-
Get Fee Quote
const quote = await provider.getWithdrawalFeeQuote(amountSats, destinationAddress) // Returns: { slow, medium, fast, expiresAt } // Each speed includes: { networkFee, totalFee, estimatedTime }
-
Execute Withdrawal
const result = await provider.withdrawToL1({ amountSats: 100000, destinationAddress: 'bc1q...', speed: 'MEDIUM', // 'SLOW', 'MEDIUM', or 'FAST' feeQuoteId: quote.feeQuoteId }) // Returns: { requestId, status, amount }
-
Check Status
const status = await provider.getWithdrawalStatus(requestId) // Returns: { id, status, txId, completedAt } // Status: 'PENDING', 'PROCESSING', 'COMPLETED', 'FAILED'
L1BitcoinReceive.vue
- Displays Bitcoin deposit address with QR code
- Polls for pending deposits every 30 seconds
- Shows confirmation progress (0/3 → 3/3)
- Claim dialog with fee breakdown
- High-fee warning when fee > 50% of deposit
L1BitcoinWithdraw.vue
- Address input with validation
- Amount entry with balance check
- Fee speed selector (Slow/Medium/Fast)
- Real-time fee estimation from mempool.space
- Withdrawal confirmation dialog
// Bitcoin address patterns supported for withdrawal
const patterns = {
p2pkh: /^1[a-km-zA-HJ-NP-Z1-9]{25,34}$/, // Legacy
p2sh: /^3[a-km-zA-HJ-NP-Z1-9]{25,34}$/, // SegWit compatible
bech32: /^bc1q[a-z0-9]{38,58}$/, // Native SegWit
bech32m: /^bc1p[a-z0-9]{58}$/ // Taproot
}Central store for all wallet operations.
State
{
wallets: [], // Array of wallet objects
activeWalletId: null, // Currently selected wallet
sessionPin: null, // PIN for current session (Spark only)
balances: {}, // Cached balances by wallet ID
isLoading: false,
error: null
}Key Actions
// Wallet management
addSparkWallet({ mnemonic, pin, name, network })
addNWCWallet({ nwcUrl, name })
removeWallet(walletId)
setActiveWallet(walletId)
// Spark security
unlockSparkWallet(pin) // Decrypt mnemonic, cache PIN
getDecryptedMnemonic(walletId) // Requires unlocked state
// Wallet operations
getWalletProvider(walletId) // Returns initialized provider
refreshBalance(walletId)Getters
activeWallet // Current wallet object
activeBalance // Balance for active wallet
isActiveWalletSpark // Boolean check
isActiveWalletNWC // Boolean check
isActiveWalletLNBits // Boolean check
activeSparkAddress // Spark address (if Spark wallet)
hasSparkWallet // Whether any Spark wallet existsWallet Types
// WALLET_TYPES constant from WalletFactory.js
WALLET_TYPES = {
SPARK: 'spark',
NWC: 'nwc',
LNBITS: 'lnbits'
}Manages saved contacts.
State
{
entries: [], // Contact objects
searchQuery: '' // Filter string
}Entry Structure
{
id: 'addr-123...',
name: 'Alice',
address: 'alice@wallet.com', // Universal field
addressType: 'lightning', // 'lightning' or 'spark'
lightningAddress: 'alice@...', // Backward compatibility
color: '#3B82F6',
createdAt: 1699999999999,
updatedAt: 1699999999999
}Key Actions
addEntry({ name, address, addressType, color })
updateEntry(id, updateData)
deleteEntry(id)
importEntries(entries) // Bulk import with validation
// Validation helpers
isValidAddress(address, type)
isValidLightningAddress(address)
isValidSparkAddress(address)
detectAddressType(address)For user-facing documentation on these features, see Sending Bitcoin and Receiving Bitcoin in the User Guide.
Handles payment input and QR scanning.
Key Features
- Camera-based QR scanning with
qr-scannerlibrary - Automatic payment type detection
- Contact picker with wallet compatibility filtering
- Clipboard paste support
Payment Detection Logic
determinePaymentType(input) {
if (input.startsWith('sp1') || input.startsWith('tsp1')) {
return { type: 'spark_address', sparkAddress: input }
}
if (input.startsWith('lnbc') || input.startsWith('lntb')) {
return { type: 'lightning_invoice', invoice: input }
}
if (input.includes('@')) {
return { type: 'lightning_address', address: input }
}
// ... LNURL handling
}Confirmation dialog before sending payments.
Key Features
- Shows payment details (recipient, amount, fees)
- Wallet compatibility warnings (Spark required for Spark addresses)
- Calls appropriate provider method based on payment type
Generates invoices and displays receive addresses.
Key Features
- Toggle between Lightning invoice and Spark address (Spark wallets)
- QR code generation with
@chenfengyuan/vue-qrcode - Copy and share functionality
- Invoice expiry countdown
Numeric PIN input for Spark wallet security.
Key Features
- 6-digit PIN with visual feedback
- Confirm mode for new PIN setup
- Numpad interface with delete button
- Update
WalletFactory.parsePaymentDestination()to detect the format - Add handling in
SendModal.determinePaymentType() - Implement the payment method in relevant providers
- Update
PaymentModal.sendPayment()to route appropriately
- Create new provider class extending
WalletProvider - Implement all required interface methods
- Update
WalletFactory.createWalletProvider()to handle new type - Add wallet creation flow in appropriate page
- Update wallet store with new add method
// stores/newStore.js
import { defineStore } from 'pinia'
export const useNewStore = defineStore('newStore', {
state: () => ({
// Initial state
}),
getters: {
// Computed properties
},
actions: {
async initialize() {
// Load from localStorage if needed
},
async persist() {
localStorage.setItem('buhoGO_new_store', JSON.stringify(this.$state))
}
}
})BuhoGO uses a consistent design system defined in src/css/app.css.
All components should support both themes using Quasar's dark mode detection:
<template>
<q-card :class="$q.dark.isActive ? 'card_dark_style' : 'card_light_style'">
<q-btn :class="$q.dark.isActive ? 'dialog_add_btn_dark' : 'dialog_add_btn_light'">
Submit
</q-btn>
</q-card>
</template>| Purpose | Dark Class | Light Class |
|---|---|---|
| Card container | card_dark_style |
card_light_style |
| Primary button | dialog_add_btn_dark |
dialog_add_btn_light |
| Secondary button | more_btn_dark |
more_btn_light |
| Dialog backdrop | dailog_dark |
dailog_light |
| Text input | add_wallet_textbox_dark |
add_wallet_textbox_light |
| Back button | back_btn_dark |
back_btn_light |
| Page title | main_page_title_dark |
main_page_title_light |
| Color | Hex | Usage |
|---|---|---|
| Primary gradient | #059573 to #78D53C |
Primary buttons |
| Accent green | #15DE72 |
Success, receive amounts |
| Error red | #FF0000 |
Errors, send amounts |
| Warning | #AFF24B |
Warnings |
| Dark background | #0C0C0C |
Dark mode backgrounds |
| Dark input | #171717 |
Dark mode inputs |
| Light background | #FFFFFF, #F8F8F8 |
Light mode backgrounds |
| Element | Radius |
|---|---|
| Cards | 24px |
| Buttons | 24px |
| Inputs | 20px |
| Menus | 16px |
| Back buttons | 10px |
Review the User Guide to understand expected user flows before testing.
# Development with hot reload
npm run dev
# Production build
npm run build
# Serve production build locally
npx serve dist/spaFor user-facing security information and best practices, see Security in the README.
Mnemonics are encrypted using Web Crypto API:
- PIN is used to derive key via PBKDF2 (100,000 iterations)
- Random salt and IV generated for each encryption
- AES-256-GCM provides authenticated encryption
- Encrypted data stored as:
salt:iv:ciphertext(base64)
// Encryption flow (simplified)
const keyMaterial = await crypto.subtle.importKey('raw', pinBuffer, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
)
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data)- Never log sensitive data: No console.log of mnemonics, PINs, or private keys
- Clear sensitive state: Session PIN is memory-only, cleared on app close
- Validate all inputs: Especially payment destinations and amounts
- Use constant-time comparisons: For PIN verification where applicable
Key settings in quasar.config.js:
build: {
target: { browser: ['es2022'] },
vueRouterMode: 'history'
}iOS configuration in src-capacitor/:
# Build for iOS
quasar build -m capacitor -T ios
# Open in Xcode
npx cap open iosFor user-facing troubleshooting, see Troubleshooting in the User Guide.
Web Crypto API requires secure context (HTTPS or localhost). For local development, use localhost not IP address.
- Check network setting matches intended environment (MAINNET/REGTEST)
- Verify mnemonic is valid 12-word BIP39
- Check console for specific SDK errors
- Verify NWC URL is complete and valid
- Check wallet provider service is running
- Try generating fresh NWC connection string
- Verify server URL includes protocol (
https://) - Check API key is valid
- Ensure LNBits server is accessible
- For self-hosted: verify CORS is configured correctly
- Check server logs for detailed error messages
- Verify wallet has sufficient balance
- Check LNBits backend has liquidity
- Invoice may have expired - request new one
- Server may be experiencing issues - check status
- Vue.js Documentation
- Quasar Framework
- Pinia State Management
- Spark SDK
- Alby SDK (NWC)
- NIP-47 Specification
- README - Project overview, features, and quick start
- User Guide - Step-by-step instructions for end users
- Use Cases - Real-world scenarios and examples
Documentation maintained by the BuhoGO development team.