mirror of
https://github.com/aljazceru/tiny-spark.git
synced 2026-01-17 06:44:54 +01:00
tiny spark
This commit is contained in:
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