mirror of
https://github.com/aljazceru/tiny-spark.git
synced 2025-12-19 08:44:21 +01:00
tiny spark
This commit is contained in:
8
.env.example
Normal file
8
.env.example
Normal file
@@ -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=
|
||||||
|
|
||||||
227
README.md
Normal file
227
README.md
Normal file
@@ -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 <payment_id>
|
||||||
|
|
||||||
|
# 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 <payment_hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 <type> <amount> [desc]` | Create payment request | `./tiny-spark receive lightning 5000 "Payment"` |
|
||||||
|
| `send <type> <dest> <amount>` | Send payment | `./tiny-spark send lightning lnbc1... 5000` |
|
||||||
|
| `payment <id>` | 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
49
config/config.go
Normal file
49
config/config.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
10
go.mod
Normal file
10
go.mod
Normal file
@@ -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
|
||||||
314
main.go
Normal file
314
main.go
Normal file
@@ -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 <type> <amount> [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 <type> <destination> <amount>")
|
||||||
|
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 <payment_id>")
|
||||||
|
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 <command> [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 <type> <amount> [desc] Create payment request")
|
||||||
|
fmt.Println(" send <type> <dest> <amount> Send payment")
|
||||||
|
fmt.Println(" payment <id> 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] + "..."
|
||||||
|
}
|
||||||
548
wallet/wallet.go
Normal file
548
wallet/wallet.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user