commit 98fbbfd0a57cfb935fbda71f1713e6c8daf598a2 Author: Aljaz Ceru Date: Tue Nov 4 11:06:50 2025 +0100 tiny spark diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..12dc26e --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Breez SDK Configuration (required) +BREEZ_API_KEY=your-breez-api-key +BREEZ_MNEMONIC="your twelve word mnemonic phrase here" + +# Optional +# Defaults to .tiny-spark-data +#BREEZ_WORKING_DIR= + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef66560 --- /dev/null +++ b/README.md @@ -0,0 +1,227 @@ +# tiny-spark + +A CLI client for spark, implementing all major Lightning Network and Bitcoin payment features based on [Breez Nodeless SDK](https://sdk-doc-spark.breez.technology/) +## Features + +### Core Wallet Operations +- **Balance Query**: Display Lightning wallet balance and spendable limits +- **Transaction History**: View and filter transaction history with detailed status +- **Payment Details**: Retrieve specific payment information by ID + +### Payment Reception +- **Lightning Invoices**: Create BOLT11 invoices for receiving Lightning payments +- **Bitcoin Addresses**: Generate on-chain Bitcoin addresses for deposits +- **Spark Addresses**: Create Spark addresses for instant zero-fee transfers + +### Payment Sending +- **Lightning Payments**: Pay BOLT11 invoices via Lightning Network +- **On-chain Bitcoin**: Send Bitcoin to any on-chain address with configurable fees +- **Spark Transfers**: Send to Spark addresses for instant settlement +- **LNURL Support**: Pay LNURL addresses and Lightning addresses + +### Token Support +- **Token Balances**: View balances for all supported tokens in the wallet +- **Token Metadata**: Access token information including names, tickers, and decimals + +## Installation + +```bash +# Clone the repository +git clone https://github.com/aljazceru/tiny-spark.git +cd tiny-spark + +# Build the client +go build -o tiny-spark + +# Ensure your .env file contains BREEZ_API_KEY and BREEZ_MNEMONIC +``` + +## Configuration + +The client uses environment variables for configuration. **Only `BREEZ_API_KEY` and `BREEZ_MNEMONIC` are required** - all other variables have sensible defaults. + +```bash +# Required variables - Must be set +BREEZ_API_KEY=your_breez_api_key +BREEZ_MNEMONIC="your twelve word mnemonic phrase" +``` + +## Usage + +### Basic Commands + +```bash +# Show wallet balance +./tiny-spark balance + +# Show transaction history (default 10 transactions) +./tiny-spark transactions +./tiny-spark transactions 20 # Show last 20 transactions + +# Show token balances +./tiny-spark tokens + +# Get specific payment details +./tiny-spark payment + +# Show help +./tiny-spark help +``` + +### Receiving Payments + +```bash +# Create Lightning invoice +./tiny-spark receive lightning 5000 "Coffee payment" + +# Create Bitcoin address +./tiny-spark receive bitcoin + +# Create Spark address +./tiny-spark receive spark +``` + +### Sending Payments + +```bash +# Pay Lightning invoice +./tiny-spark send lightning lnbc1... 5000 + +# Send to Bitcoin address +./tiny-spark send bitcoin bc1q... 50000 + +# Send to Spark address +./tiny-spark send spark spark... 25000 + +# Pay LNURL address +./tiny-spark send lnurl user@example.com 5000 +``` + +## Examples + +### Daily Operations + +```bash +# Check current balance +./tiny-spark balance + +# Create invoice for receiving payment +./tiny-spark receive lightning 10000 "Web development services" + +# Check recent transactions +./tiny-spark transactions 5 + +# Pay an invoice +./tiny-spark send lightning lnbc1... 10000 + +# Verify payment status +./tiny-spark payment +``` + +### Business Integration + +```bash +# Generate payment address for customer +./tiny-spark receive lightning 25000 "Invoice #12345" + +# Accept Bitcoin payment +./tiny-spark receive bitcoin + +# Send payment to supplier +./tiny-spark send bitcoin bc1q... 100000 + +# Check all token balances +./tiny-spark tokens +``` + +## Command Reference + +| Command | Description | Example | +|---------|-------------|---------| +| `balance` | Show wallet balance and limits | `./tiny-spark balance` | +| `transactions [N]` | Show last N transactions | `./tiny-spark transactions 15` | +| `receive [desc]` | Create payment request | `./tiny-spark receive lightning 5000 "Payment"` | +| `send ` | Send payment | `./tiny-spark send lightning lnbc1... 5000` | +| `payment ` | Show payment details | `./tiny-spark payment abc123...` | +| `tokens` | Show token balances | `./tiny-spark tokens` | + +### Payment Types + +**Receive Types:** +- `lightning` / `ln` - Create BOLT11 Lightning invoice +- `bitcoin` / `btc` - Generate Bitcoin address +- `spark` - Create Spark address + +**Send Types:** +- `lightning` / `ln` - Pay Lightning invoice +- `bitcoin` / `btc` - Send to Bitcoin address +- `spark` - Send to Spark address +- `lnurl` - Pay LNURL/Lightning address + +## Example Output + +``` +Breez Tiny Spark +================== + +Wallet Balance: +---------------- +Lightning Balance: 5000 sats +Max Payable: 5000 sats +Max Receivable: 5000 sats + +Last 10 Transactions: +-------------------- +TIME TYPE AMOUNT FEE STATUS DESCRIPTION +---- ---- ------ --- ------ ----------- +2025-11-01 15:36 receive +12 +3 Complete Payment +2025-10-31 15:56 receive +5 0 Complete Payment +2025-10-31 15:55 receive +20 +1 Complete Payment +2025-10-31 09:15 receive +100 0 Complete Payment + +Payment Request Created: +Type: Lightning +Amount: 5000 sats +Fee: 0 sats +Description: Coffee payment +Expires: 2025-11-05 10:19:36 + +Payment Request: +lnbc50u1p5sn3fgpp5f432vrt88n6876wt6kx7en8xj7kv99rh7qd9fcm793y7y7vz92sssp5xk2etegmu098jnza9aspfkgg39tm5ar2lndmpyjzd3ynuts8n2rqxq9z0rgqnp4qvyndeaqzman7h898jxm98dzkm0mlrsx36s93smrur7h0azyyuxc5rzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqrt49lmtcqqqqqqqqqqq86qq9qcqzpgdq523jhxapqwpshjmt9de6q9qyyssqv30v9dmqjgjgnc2xupsvhhmyqtjgf2tm3mgh9gqxwrfhef4yamczn6hauvvwzqwxhda6mdrjamcg72rz2f7nrrgwkllnf40x0703yecq298zxl +``` + +## Security Considerations + +- Keep your mnemonic and API key secure +- Use mainnet only for production transactions +- Verify payment details before sending +- Consider using testnet for development and testing + +## Development + +This client is based on the official Breez SDK Spark Go examples and implements: + +- **SDK Initialization**: Proper SDK connection and configuration +- **Error Handling**: Comprehensive error handling with user-friendly messages +- **Type Safety**: Proper Go type handling for all SDK operations +- **Transaction Management**: Complete payment lifecycle support +- **Multi-Asset Support**: Bitcoin and token balance management + +## Requirements + +- Go 1.21+ +- Breez API credentials (`BREEZ_API_KEY`) +- Valid mnemonic phrase (`BREEZ_MNEMONIC`) +- Network connectivity for Lightning/Bitcoin operations + +**Environment Setup:** +```bash +# Minimum required .env file +BREEZ_API_KEY=your_breez_api_key +BREEZ_MNEMONIC="your twelve word mnemonic phrase" + +# Run the application +./tiny-spark +``` + + diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..4a062ba --- /dev/null +++ b/config/config.go @@ -0,0 +1,49 @@ +package config + +import ( + "fmt" + "os" + + "github.com/joho/godotenv" +) + +type Config struct { + BreezAPIKey string + BreezMnemonic string + BreezNetwork string + BreezWorkingDir string +} + +// LoadConfig loads configuration from environment variables +func LoadConfig() (*Config, error) { + // Try to load .env file, but don't fail if it doesn't exist + if err := godotenv.Load(); err != nil { + fmt.Printf("Warning: Could not load .env file: %v\n", err) + fmt.Println("Using environment variables from system") + } + + config := &Config{ + BreezAPIKey: getEnv("BREEZ_API_KEY", ""), + BreezMnemonic: getEnv("BREEZ_MNEMONIC", ""), + BreezNetwork: getEnv("BREEZ_NETWORK", "mainnet"), + BreezWorkingDir: getEnv("BREEZ_WORKING_DIR", getEnv("BREEZ_DATA_DIR", ".tiny-spark-data")), + } + + // Validate only required fields + if config.BreezAPIKey == "" { + return nil, fmt.Errorf("BREEZ_API_KEY is required") + } + if config.BreezMnemonic == "" { + return nil, fmt.Errorf("BREEZ_MNEMONIC is required") + } + + return config, nil +} + +// getEnv gets an environment variable with a default value +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e04fae8 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/breez/tiny-spark + +go 1.21 + +require ( + github.com/breez/breez-sdk-spark-go v0.0.0-00010101000000-000000000000 + github.com/joho/godotenv v1.4.0 +) + +replace github.com/breez/breez-sdk-spark-go => ../breez-sdk-spark-go diff --git a/main.go b/main.go new file mode 100644 index 0000000..b3f4650 --- /dev/null +++ b/main.go @@ -0,0 +1,314 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + "strings" + "text/tabwriter" + + "github.com/breez/tiny-spark/config" + "github.com/breez/tiny-spark/wallet" +) + +func main() { + if len(os.Args) < 2 { + printUsage() + return + } + + command := os.Args[1] + + // Load configuration + cfg, err := config.LoadConfig() + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + + // Initialize wallet + w, err := wallet.NewWallet(cfg) + if err != nil { + log.Fatalf("Failed to initialize wallet: %v", err) + } + defer w.Close() + + ctx := context.Background() + + switch command { + case "balance", "bal": + showBalance(ctx, w) + case "transactions", "tx": + limit := 10 + if len(os.Args) > 2 { + if l, err := strconv.Atoi(os.Args[2]); err == nil { + limit = l + } + } + showTransactions(ctx, w, limit) + case "receive": + if len(os.Args) < 4 { + fmt.Println("Usage: tiny-client receive [description]") + fmt.Println("Types: lightning, bitcoin, spark") + return + } + receivePayment(ctx, w, os.Args[2], os.Args[3], strings.Join(os.Args[4:], " ")) + case "send": + if len(os.Args) < 4 { + fmt.Println("Usage: tiny-client send ") + fmt.Println("Types: lightning, bitcoin, spark, lnurl") + return + } + sendPayment(ctx, w, os.Args[2], os.Args[3], os.Args[4]) + case "payment": + if len(os.Args) < 3 { + fmt.Println("Usage: tiny-client payment ") + return + } + showPayment(ctx, w, os.Args[2]) + case "tokens": + showTokens(ctx, w) + case "help", "-h", "--help": + printUsage() + default: + fmt.Printf("Unknown command: %s\n\n", command) + printUsage() + } +} + +func printUsage() { + fmt.Println("Breez Tiny Spark") + fmt.Println("==================") + fmt.Println() + fmt.Println("Usage:") + fmt.Println(" tiny-spark [arguments]") + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" balance, bal Show wallet balance") + fmt.Println(" transactions, tx [limit] Show transaction history (default 10)") + fmt.Println(" receive [desc] Create payment request") + fmt.Println(" send Send payment") + fmt.Println(" payment Show payment details") + fmt.Println(" tokens Show token balances") + fmt.Println(" help Show this help") + fmt.Println() + fmt.Println("Receive types:") + fmt.Println(" lightning Create Lightning invoice") + fmt.Println(" bitcoin Create Bitcoin address") + fmt.Println(" spark Create Spark address") + fmt.Println() + fmt.Println("Send types:") + fmt.Println(" lightning Pay Lightning invoice") + fmt.Println(" bitcoin Send to Bitcoin address") + fmt.Println(" spark Send to Spark address") + fmt.Println(" lnurl Pay LNURL address") + fmt.Println() + fmt.Println("Examples:") + fmt.Println(" tiny-spark balance") + fmt.Println(" tiny-spark receive lightning 5000 'Coffee payment'") + fmt.Println(" tiny-spark send lightning lnbc1... 5000") + fmt.Println(" tiny-spark transactions 20") +} + +func showBalance(ctx context.Context, w *wallet.Wallet) { + fmt.Println("Wallet Balance:") + fmt.Println("----------------") + balance, err := w.GetBalance(ctx) + if err != nil { + log.Fatalf("Failed to get balance: %v", err) + } + + fmt.Printf("Lightning Balance: %d sats\n", balance.LightningBalanceSats) + fmt.Printf("Max Payable: %d sats\n", balance.MaxPayableSats) + fmt.Printf("Max Receivable: %d sats\n", balance.MaxReceivableSats) +} + +func showTransactions(ctx context.Context, w *wallet.Wallet, limit int) { + fmt.Printf("Last %d Transactions:\n", limit) + fmt.Println(strings.Repeat("-", 20)) + + transactions, err := w.GetTransactions(ctx, limit) + if err != nil { + log.Fatalf("Failed to get transactions: %v", err) + } + + if len(transactions) == 0 { + fmt.Println("No transactions found") + return + } + + // Use tabwriter for nice formatting + tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(tabWriter, "TIME\tTYPE\tAMOUNT\tFEE\tSTATUS\tDESCRIPTION") + fmt.Fprintln(tabWriter, "----\t----\t------\t---\t------\t-----------") + + for _, tx := range transactions { + timestamp := tx.Timestamp.Format("2006-01-02 15:04") + amountStr := formatAmount(tx.AmountSats) + feeStr := formatAmount(tx.FeeSats) + description := truncateString(tx.Description, 20) + if description == "" { + description = "-" + } + + fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\t%s\t%s\n", + timestamp, tx.Type, amountStr, feeStr, tx.Status, description) + } + tabWriter.Flush() +} + +func receivePayment(ctx context.Context, w *wallet.Wallet, paymentType, amountStr, description string) { + amount, err := strconv.ParseUint(amountStr, 10, 64) + if err != nil { + log.Fatalf("Invalid amount: %v", err) + } + + if description == "" { + description = "Payment request" + } + + var response *wallet.ReceivePaymentResponse + + switch strings.ToLower(paymentType) { + case "lightning", "ln": + response, err = w.ReceiveLightningInvoice(ctx, amount, description) + case "bitcoin", "btc": + response, err = w.ReceiveBitcoinAddress(ctx) + case "spark": + response, err = w.ReceiveSparkAddress(ctx) + default: + log.Fatalf("Unknown receive type: %s", paymentType) + } + + if err != nil { + log.Fatalf("Failed to create %s payment request: %v", paymentType, err) + } + + fmt.Printf("Payment Request Created:\n") + fmt.Printf("Type: %s\n", strings.Title(paymentType)) + fmt.Printf("Amount: %d sats\n", response.AmountSats) + fmt.Printf("Fee: %d sats\n", response.FeeSats) + fmt.Printf("Description: %s\n", response.Description) + fmt.Printf("Expires: %s\n", response.ExpiresAt.Format("2006-01-02 15:04:05")) + fmt.Printf("\nPayment Request:\n%s\n", response.PaymentRequest) +} + +func sendPayment(ctx context.Context, w *wallet.Wallet, paymentType, destination, amountStr string) { + var response *wallet.PaymentResponse + var err error + + switch strings.ToLower(paymentType) { + case "lightning", "ln": + response, err = w.SendLightningInvoice(ctx, destination) + case "bitcoin", "btc": + amount, err2 := strconv.ParseInt(amountStr, 10, 64) + if err2 != nil { + log.Fatalf("Invalid amount: %v", err2) + } + response, err = w.SendBitcoinAddress(ctx, destination, amount) + case "spark": + amount, err2 := strconv.ParseInt(amountStr, 10, 64) + if err2 != nil { + log.Fatalf("Invalid amount: %v", err2) + } + response, err = w.SendSparkAddress(ctx, destination, amount) + case "lnurl": + amount, err2 := strconv.ParseUint(amountStr, 10, 64) + if err2 != nil { + log.Fatalf("Invalid amount: %v", err2) + } + response, err = w.LnUrlPay(ctx, destination, amount, "Payment via LNURL") + default: + log.Fatalf("Unknown send type: %s", paymentType) + } + + if err != nil { + log.Fatalf("Failed to send %s payment: %v", paymentType, err) + } + + fmt.Printf("Payment Sent:\n") + fmt.Printf("Payment Hash: %s\n", response.PaymentHash) + fmt.Printf("Amount: %d sats\n", response.AmountSats) + fmt.Printf("Fee: %d sats\n", response.FeeSats) + fmt.Printf("Status: %s\n", response.Status) + fmt.Printf("Completed: %s\n", response.CompletedAt.Format("2006-01-02 15:04:05")) +} + +func showPayment(ctx context.Context, w *wallet.Wallet, paymentID string) { + payment, err := w.GetPayment(ctx, paymentID) + if err != nil { + log.Fatalf("Failed to get payment: %v", err) + } + + fmt.Printf("Payment Details:\n") + fmt.Printf("ID: %s\n", payment.ID) + fmt.Printf("Type: %s\n", payment.Type) + fmt.Printf("Amount: %s sats\n", formatAmount(payment.AmountSats)) + fmt.Printf("Fee: %s sats\n", formatAmount(payment.FeeSats)) + fmt.Printf("Status: %s\n", payment.Status) + fmt.Printf("Description: %s\n", payment.Description) + fmt.Printf("Time: %s\n", payment.Timestamp.Format("2006-01-02 15:04:05")) +} + +func showTokens(ctx context.Context, w *wallet.Wallet) { + fmt.Println("Token Balances:") + fmt.Println("---------------") + + tokens, err := w.GetTokenBalances(ctx) + if err != nil { + log.Fatalf("Failed to get token balances: %v", err) + } + + if len(tokens) == 0 { + fmt.Println("No tokens found") + return + } + + tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(tabWriter, "TOKEN ID\tNAME\tTICKER\tBALANCE") + fmt.Fprintln(tabWriter, "---------\t----\t------\t-------") + + for _, token := range tokens { + fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\n", + token.TokenID, token.Name, token.Ticker, token.Balance) + } + tabWriter.Flush() +} + +// formatAmount formats satoshi amount with proper sign +func formatAmount(sats int64) string { + if sats == 0 { + return "0" + } + + if sats > 0 { + return fmt.Sprintf("+%d", sats) + } + return fmt.Sprintf("%d", sats) +} + +// formatStatus makes the status more readable +func formatStatus(status string) string { + switch status { + case "complete": + return "Complete" + case "pending": + return "Pending" + case "failed": + return "Failed" + default: + return status + } +} + +// truncateString truncates a string to max length with ellipsis if needed +func truncateString(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + if maxLen <= 3 { + return s[:maxLen] + } + return s[:maxLen-3] + "..." +} \ No newline at end of file diff --git a/wallet/wallet.go b/wallet/wallet.go new file mode 100644 index 0000000..35efa78 --- /dev/null +++ b/wallet/wallet.go @@ -0,0 +1,548 @@ +package wallet + +import ( + "context" + "fmt" + "math/big" + "os" + "time" + + breez_sdk_common "github.com/breez/breez-sdk-spark-go/breez_sdk_common" + breez_sdk_spark "github.com/breez/breez-sdk-spark-go/breez_sdk_spark" + "github.com/breez/tiny-spark/config" +) + +type Wallet struct { + sdk *breez_sdk_spark.BreezSdk + config *config.Config +} + +type Balance struct { + LightningBalanceSats int64 + MaxPayableSats int64 + MaxReceivableSats int64 +} + +type Transaction struct { + ID string + AmountSats int64 + FeeSats int64 + Status string + Type string + Description string + Timestamp time.Time + PaymentHash string +} + +type ReceivePaymentResponse struct { + PaymentRequest string + AmountSats int64 + FeeSats int64 + Description string + ExpiresAt time.Time +} + +type PaymentResponse struct { + PaymentHash string + AmountSats int64 + FeeSats int64 + Status string + Preimage string + CompletedAt time.Time +} + +type TokenBalance struct { + TokenID string + Balance string + Name string + Ticker string + Decimals int +} + +// NewWallet initializes a new Breez SDK wallet +func NewWallet(cfg *config.Config) (*Wallet, error) { + // Create working directory if it doesn't exist + if err := createWorkingDir(cfg.BreezWorkingDir); err != nil { + return nil, fmt.Errorf("failed to create working directory: %w", err) + } + + // Create SDK configuration + network := networkFromString(cfg.BreezNetwork) + sdkConfig := breez_sdk_spark.DefaultConfig(network) + if sdkConfig.ApiKey != nil { + *sdkConfig.ApiKey = cfg.BreezAPIKey + } else { + sdkConfig.ApiKey = &cfg.BreezAPIKey + } + sdkConfig.SyncIntervalSecs = 60 // Use longer sync interval for better data + + // Create seed from mnemonic + seed := breez_sdk_spark.SeedMnemonic{ + Mnemonic: cfg.BreezMnemonic, + } + + // Connect to SDK + request := breez_sdk_spark.ConnectRequest{ + Config: sdkConfig, + Seed: seed, + StorageDir: cfg.BreezWorkingDir, + } + + sdk, err := breez_sdk_spark.Connect(request) + + // Handle error using official SDK pattern + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to connect to Breez SDK: %w", err) + } + + // Wait longer for initial sync + time.Sleep(10 * time.Second) + + wallet := &Wallet{ + sdk: sdk, + config: cfg, + } + + return wallet, nil +} + +// Close closes the SDK connection +func (w *Wallet) Close() error { + if w.sdk != nil { + return w.sdk.Disconnect() + } + return nil +} + +// GetBalance retrieves the wallet balance +func (w *Wallet) GetBalance(ctx context.Context) (*Balance, error) { + req := breez_sdk_spark.GetInfoRequest{} + info, err := w.sdk.GetInfo(req) + + // Handle error using official SDK pattern + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to get wallet info: %w", err) + } + + // Try both BalanceSats and TokenBalances + balanceSats := int64(info.BalanceSats) + + // Check if there are token balances (might be the actual Lightning balance) + if len(info.TokenBalances) > 0 { + for _, balance := range info.TokenBalances { + tokenBalance := balance.Balance.Int64() + if tokenBalance > 0 { + balanceSats = tokenBalance + break + } + } + } + + return &Balance{ + LightningBalanceSats: balanceSats, + MaxPayableSats: balanceSats, + MaxReceivableSats: balanceSats, + }, nil +} + +// GetTransactions retrieves transaction history +func (w *Wallet) GetTransactions(ctx context.Context, limit int) ([]*Transaction, error) { + offsetPtr := uint32(0) + limitPtr := uint32(limit) + if limitPtr < 10 { + limitPtr = 100 // Use higher limit like the WebAssembly example + } + + // Simple request structure exactly matching the WebAssembly example + req := breez_sdk_spark.ListPaymentsRequest{ + Offset: &offsetPtr, + Limit: &limitPtr, + } + + response, err := w.sdk.ListPayments(req) + + // Handle error using official SDK pattern + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to get transaction history: %w", err) + } + + transactions := make([]*Transaction, len(response.Payments)) + for i, payment := range response.Payments { + var txType string + + // Get raw amounts from SDK + rawAmount := payment.Amount.Int64() + fee := payment.Fees.Int64() + var amount int64 + + // Use PaymentType enum for classification and amount sign correction + switch payment.PaymentType { + case breez_sdk_spark.PaymentTypeReceive: + txType = "receive" + // Keep amount positive for receive transactions + amount = rawAmount + case breez_sdk_spark.PaymentTypeSend: + txType = "send" + // Make amount negative for send transactions + amount = -rawAmount + default: + // Fallback to amount-based classification + if rawAmount > 0 { + txType = "receive" + amount = rawAmount + } else { + txType = "send" + amount = rawAmount + } + } + + // Convert payment status to readable format + var statusStr string + switch payment.Status { + case breez_sdk_spark.PaymentStatusPending: + statusStr = "Pending" + case breez_sdk_spark.PaymentStatusCompleted: + statusStr = "Complete" + case breez_sdk_spark.PaymentStatusFailed: + statusStr = "Failed" + default: + statusStr = string(payment.Status) + } + + // Use generic description for now + description := "Payment" + + transactions[i] = &Transaction{ + ID: payment.Id, + AmountSats: amount, + FeeSats: fee, + Status: statusStr, + Type: txType, + Description: description, + Timestamp: time.Unix(int64(payment.Timestamp), 0), + PaymentHash: payment.Id, + } + } + + return transactions, nil +} + +// ReceiveLightningInvoice creates a Lightning invoice for receiving payments +func (w *Wallet) ReceiveLightningInvoice(ctx context.Context, amountSats uint64, description string) (*ReceivePaymentResponse, error) { + request := breez_sdk_spark.ReceivePaymentRequest{ + PaymentMethod: breez_sdk_spark.ReceivePaymentMethodBolt11Invoice{ + Description: description, + AmountSats: &amountSats, + }, + } + + response, err := w.sdk.ReceivePayment(request) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to create lightning invoice: %w", err) + } + + return &ReceivePaymentResponse{ + PaymentRequest: response.PaymentRequest, + FeeSats: response.Fee.Int64(), + AmountSats: int64(amountSats), + Description: description, + ExpiresAt: time.Now().Add(24 * time.Hour), + }, nil +} + +// ReceiveBitcoinAddress creates a Bitcoin address for receiving on-chain payments +func (w *Wallet) ReceiveBitcoinAddress(ctx context.Context) (*ReceivePaymentResponse, error) { + request := breez_sdk_spark.ReceivePaymentRequest{ + PaymentMethod: breez_sdk_spark.ReceivePaymentMethodBitcoinAddress{}, + } + + response, err := w.sdk.ReceivePayment(request) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to create bitcoin address: %w", err) + } + + return &ReceivePaymentResponse{ + PaymentRequest: response.PaymentRequest, + FeeSats: response.Fee.Int64(), + AmountSats: 0, // User-specified amount + Description: "Bitcoin address deposit", + ExpiresAt: time.Now().Add(24 * time.Hour), + }, nil +} + +// ReceiveSparkAddress creates a Spark address for receiving payments +func (w *Wallet) ReceiveSparkAddress(ctx context.Context) (*ReceivePaymentResponse, error) { + request := breez_sdk_spark.ReceivePaymentRequest{ + PaymentMethod: breez_sdk_spark.ReceivePaymentMethodSparkAddress{}, + } + + response, err := w.sdk.ReceivePayment(request) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to create spark address: %w", err) + } + + return &ReceivePaymentResponse{ + PaymentRequest: response.PaymentRequest, + FeeSats: response.Fee.Int64(), + AmountSats: 0, + Description: "Spark address deposit", + ExpiresAt: time.Now().Add(24 * time.Hour), + }, nil +} + +// SendLightningInvoice pays a Lightning invoice +func (w *Wallet) SendLightningInvoice(ctx context.Context, bolt11 string) (*PaymentResponse, error) { + // Prepare the payment first + prepareReq := breez_sdk_spark.PrepareSendPaymentRequest{ + PaymentRequest: bolt11, + Amount: nil, // Let SDK determine amount from invoice + } + + prepareResp, err := w.sdk.PrepareSendPayment(prepareReq) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to prepare lightning payment: %w", err) + } + + // Send the payment + sendReq := breez_sdk_spark.SendPaymentRequest{ + PrepareResponse: prepareResp, + } + + response, err := w.sdk.SendPayment(sendReq) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to send lightning payment: %w", err) + } + + return &PaymentResponse{ + PaymentHash: response.Payment.Id, + AmountSats: response.Payment.Amount.Int64(), + FeeSats: response.Payment.Fees.Int64(), + Status: string(response.Payment.Status), + CompletedAt: time.Unix(int64(response.Payment.Timestamp), 0), + }, nil +} + +// SendBitcoinAddress sends Bitcoin to an on-chain address +func (w *Wallet) SendBitcoinAddress(ctx context.Context, address string, amountSats int64) (*PaymentResponse, error) { + // Convert int64 to big.Int for SDK + amount := big.NewInt(amountSats) + + // Prepare the payment + prepareReq := breez_sdk_spark.PrepareSendPaymentRequest{ + PaymentRequest: address, + Amount: &amount, + } + + prepareResp, err := w.sdk.PrepareSendPayment(prepareReq) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to prepare onchain payment: %w", err) + } + + // Send the payment with medium confirmation speed + var options breez_sdk_spark.SendPaymentOptions = breez_sdk_spark.SendPaymentOptionsBitcoinAddress{ + ConfirmationSpeed: breez_sdk_spark.OnchainConfirmationSpeedMedium, + } + + sendReq := breez_sdk_spark.SendPaymentRequest{ + PrepareResponse: prepareResp, + Options: &options, + } + + response, err := w.sdk.SendPayment(sendReq) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to send onchain payment: %w", err) + } + + return &PaymentResponse{ + PaymentHash: response.Payment.Id, + AmountSats: response.Payment.Amount.Int64(), + FeeSats: response.Payment.Fees.Int64(), + Status: string(response.Payment.Status), + CompletedAt: time.Unix(int64(response.Payment.Timestamp), 0), + }, nil +} + +// SendSparkAddress sends to a Spark address +func (w *Wallet) SendSparkAddress(ctx context.Context, sparkAddress string, amountSats int64) (*PaymentResponse, error) { + // Convert int64 to big.Int for SDK + amount := big.NewInt(amountSats) + + // Prepare the payment + prepareReq := breez_sdk_spark.PrepareSendPaymentRequest{ + PaymentRequest: sparkAddress, + Amount: &amount, + } + + prepareResp, err := w.sdk.PrepareSendPayment(prepareReq) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to prepare spark payment: %w", err) + } + + // Send the payment + sendReq := breez_sdk_spark.SendPaymentRequest{ + PrepareResponse: prepareResp, + } + + response, err := w.sdk.SendPayment(sendReq) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to send spark payment: %w", err) + } + + return &PaymentResponse{ + PaymentHash: response.Payment.Id, + AmountSats: response.Payment.Amount.Int64(), + FeeSats: response.Payment.Fees.Int64(), + Status: string(response.Payment.Status), + CompletedAt: time.Unix(int64(response.Payment.Timestamp), 0), + }, nil +} + +// GetPayment retrieves a specific payment by ID +func (w *Wallet) GetPayment(ctx context.Context, paymentID string) (*Transaction, error) { + req := breez_sdk_spark.GetPaymentRequest{ + PaymentId: paymentID, + } + + response, err := w.sdk.GetPayment(req) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to get payment: %w", err) + } + + payment := response.Payment + var txType string + if payment.Amount.Int64() > 0 { + txType = "receive" + } else { + txType = "send" + } + + // Convert payment status to readable format + var statusStr string + switch payment.Status { + case breez_sdk_spark.PaymentStatusPending: + statusStr = "Pending" + case breez_sdk_spark.PaymentStatusCompleted: + statusStr = "Complete" + case breez_sdk_spark.PaymentStatusFailed: + statusStr = "Failed" + default: + statusStr = string(payment.Status) + } + + return &Transaction{ + ID: payment.Id, + AmountSats: payment.Amount.Int64(), + FeeSats: payment.Fees.Int64(), + Status: statusStr, + Type: txType, + Description: "Payment", + Timestamp: time.Unix(int64(payment.Timestamp), 0), + PaymentHash: payment.Id, + }, nil +} + +// LnUrlPay prepares and sends LNURL payments +func (w *Wallet) LnUrlPay(ctx context.Context, lnurlAddress string, amountSats uint64, comment string) (*PaymentResponse, error) { + // Parse the LNURL address + input, err := w.sdk.Parse(lnurlAddress) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to parse lnurl address: %w", err) + } + + switch inputType := input.(type) { + case breez_sdk_common.InputTypeLightningAddress: + validateSuccessActionUrl := true + + prepareReq := breez_sdk_spark.PrepareLnurlPayRequest{ + AmountSats: amountSats, + PayRequest: inputType.Field0.PayRequest, + Comment: &comment, + ValidateSuccessActionUrl: &validateSuccessActionUrl, + } + + prepareResp, err := w.sdk.PrepareLnurlPay(prepareReq) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to prepare lnurl pay: %w", err) + } + + // Send the LNURL payment + payReq := breez_sdk_spark.LnurlPayRequest{ + PrepareResponse: prepareResp, + } + + response, err := w.sdk.LnurlPay(payReq) + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to send lnurl payment: %w", err) + } + + return &PaymentResponse{ + PaymentHash: response.Payment.Id, + AmountSats: response.Payment.Amount.Int64(), + FeeSats: response.Payment.Fees.Int64(), + Status: string(response.Payment.Status), + CompletedAt: time.Unix(int64(response.Payment.Timestamp), 0), + }, nil + } + + return nil, fmt.Errorf("unsupported LNURL address type") +} + +// GetTokenBalances retrieves token balances +func (w *Wallet) GetTokenBalances(ctx context.Context) ([]*TokenBalance, error) { + ensureSynced := false + info, err := w.sdk.GetInfo(breez_sdk_spark.GetInfoRequest{ + EnsureSynced: &ensureSynced, + }) + + if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil { + return nil, fmt.Errorf("failed to get token balances: %w", err) + } + + var balances []*TokenBalance + for tokenId, tokenBalance := range info.TokenBalances { + balances = append(balances, &TokenBalance{ + TokenID: tokenId, + Balance: tokenBalance.Balance.String(), + Name: tokenBalance.TokenMetadata.Name, + Ticker: tokenBalance.TokenMetadata.Ticker, + Decimals: int(tokenBalance.TokenMetadata.Decimals), + }) + } + + return balances, nil +} + +// Helper functions + +// createWorkingDir creates the working directory if it doesn't exist +func createWorkingDir(path string) error { + if path == "" { + return fmt.Errorf("working directory path cannot be empty") + } + + // Create the directory with all necessary parent directories + if err := os.MkdirAll(path, 0755); err != nil { + return fmt.Errorf("failed to create working directory %s: %w", path, err) + } + + // Verify the directory exists and is accessible + if stat, err := os.Stat(path); err != nil { + return fmt.Errorf("working directory %s is not accessible: %w", path, err) + } else if !stat.IsDir() { + return fmt.Errorf("working directory path %s is not a directory", path) + } + + return nil +} + +// networkFromString converts network string to SDK Network type +func networkFromString(network string) breez_sdk_spark.Network { + switch network { + case "mainnet": + return breez_sdk_spark.NetworkMainnet + case "testnet": + return breez_sdk_spark.NetworkRegtest // Use regtest for testnet as fallback + default: + return breez_sdk_spark.NetworkRegtest + } +} \ No newline at end of file