transactions.go raw

   1  package api
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"errors"
   7  	"fmt"
   8  	"net/url"
   9  	"strconv"
  10  	"strings"
  11  	"time"
  12  
  13  	"github.com/getAlby/hub/constants"
  14  	"github.com/getAlby/hub/logger"
  15  	"github.com/getAlby/hub/transactions"
  16  	"github.com/sirupsen/logrus"
  17  )
  18  
  19  func (api *api) CreateInvoice(ctx context.Context, amountMsat uint64, description string, toAppId *uint) (*MakeInvoiceResponse, error) {
  20  	lnClient := api.svc.GetLNClient()
  21  	if lnClient == nil {
  22  		return nil, ErrLNClientNotStarted
  23  	}
  24  
  25  	if toAppId != nil && api.appsSvc.GetAppById(*toAppId) == nil {
  26  		return nil, errors.New("app does not exist")
  27  	}
  28  
  29  	transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, toAppId, nil, nil)
  30  	if err != nil {
  31  		return nil, err
  32  	}
  33  	return toApiTransaction(transaction), nil
  34  }
  35  
  36  func (api *api) LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error) {
  37  	lnClient := api.svc.GetLNClient()
  38  	if lnClient == nil {
  39  		return nil, ErrLNClientNotStarted
  40  	}
  41  	transaction, err := api.svc.GetTransactionsService().LookupTransaction(ctx, paymentHash, nil, lnClient, nil)
  42  	if err != nil {
  43  		return nil, err
  44  	}
  45  	return toApiTransaction(transaction), nil
  46  }
  47  
  48  func (api *api) SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error {
  49  	return api.svc.GetTransactionsService().SetTransactionUserLabels(ctx, id, labels)
  50  }
  51  
  52  // ParseListTransactionsFilters parses transaction filter query parameters
  53  // shared by the HTTP and Wails transports. Invalid values return an error.
  54  func ParseListTransactionsFilters(query url.Values) (ListTransactionsFilters, error) {
  55  	filters := ListTransactionsFilters{}
  56  
  57  	if transactionType := query.Get("type"); transactionType != "" {
  58  		if transactionType != constants.TRANSACTION_TYPE_INCOMING && transactionType != constants.TRANSACTION_TYPE_OUTGOING {
  59  			return filters, fmt.Errorf("invalid type: %s", transactionType)
  60  		}
  61  		filters.Type = &transactionType
  62  	}
  63  
  64  	if minAmountSatParam := query.Get("minAmountSat"); minAmountSatParam != "" {
  65  		minAmountSat, err := strconv.ParseUint(minAmountSatParam, 10, 64)
  66  		if err != nil || minAmountSat == 0 {
  67  			return filters, fmt.Errorf("invalid minAmountSat: %s", minAmountSatParam)
  68  		}
  69  
  70  		const msatPerSat = uint64(1000)
  71  		if minAmountSat > ^uint64(0)/msatPerSat {
  72  			return filters, fmt.Errorf("minAmountSat is too large")
  73  		}
  74  
  75  		minAmountMsat := minAmountSat * msatPerSat
  76  		filters.MinAmountMsat = &minAmountMsat
  77  	}
  78  
  79  	if hideFailedParam := query.Get("hideFailed"); hideFailedParam != "" {
  80  		hideFailed, err := strconv.ParseBool(hideFailedParam)
  81  		if err != nil {
  82  			return filters, fmt.Errorf("invalid hideFailed: %s", hideFailedParam)
  83  		}
  84  		filters.HideFailed = hideFailed
  85  	}
  86  
  87  	filters.SearchTerm = strings.TrimSpace(query.Get("search"))
  88  
  89  	return filters, nil
  90  }
  91  
  92  func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64, filters ListTransactionsFilters) (*ListTransactionsResponse, error) {
  93  	lnClient := api.svc.GetLNClient()
  94  	if lnClient == nil {
  95  		return nil, ErrLNClientNotStarted
  96  	}
  97  
  98  	forceFilterByAppId := false
  99  	if appId != nil {
 100  		forceFilterByAppId = true
 101  	}
 102  
 103  	dbTransactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, lnClient, appId, forceFilterByAppId, &transactions.ListTransactionsFilters{
 104  		Type:          filters.Type,
 105  		MinAmountMsat: filters.MinAmountMsat,
 106  		HideFailed:    filters.HideFailed,
 107  		SearchTerm:    filters.SearchTerm,
 108  	})
 109  	if err != nil {
 110  		return nil, err
 111  	}
 112  
 113  	apiTransactions := []Transaction{}
 114  	for _, transaction := range dbTransactions {
 115  		apiTransactions = append(apiTransactions, *toApiTransaction(&transaction))
 116  	}
 117  
 118  	return &ListTransactionsResponse{
 119  		Transactions: apiTransactions,
 120  		TotalCount:   totalCount,
 121  	}, nil
 122  }
 123  
 124  func (api *api) SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}, appId *uint) (*SendPaymentResponse, error) {
 125  	lnClient := api.svc.GetLNClient()
 126  	if lnClient == nil {
 127  		return nil, ErrLNClientNotStarted
 128  	}
 129  
 130  	transaction, err := api.svc.GetTransactionsService().SendPaymentSync(invoice, amountMsat, metadata, lnClient, appId, nil)
 131  	if err != nil {
 132  		return nil, err
 133  	}
 134  	return toApiTransaction(transaction), nil
 135  }
 136  
 137  func toApiTransaction(transaction *transactions.Transaction) *Transaction {
 138  
 139  	updatedAt := transaction.UpdatedAt.Format(time.RFC3339)
 140  	createdAt := transaction.CreatedAt.Format(time.RFC3339)
 141  	var settledAt *string
 142  	var preimage *string
 143  	if transaction.SettledAt != nil {
 144  		settledAtValue := transaction.SettledAt.Format(time.RFC3339)
 145  		settledAt = &settledAtValue
 146  		preimage = transaction.Preimage
 147  	}
 148  
 149  	var metadata Metadata
 150  	if transaction.Metadata != nil {
 151  		jsonErr := json.Unmarshal(transaction.Metadata, &metadata)
 152  		if jsonErr != nil {
 153  			logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{
 154  				"payment_hash": transaction.PaymentHash,
 155  				"metadata":     transaction.Metadata,
 156  			}).Error("Failed to deserialize transaction metadata")
 157  		}
 158  	}
 159  
 160  	var boostagram *Boostagram
 161  	if transaction.Boostagram != nil {
 162  		var txBoostagram transactions.Boostagram
 163  		jsonErr := json.Unmarshal(transaction.Boostagram, &txBoostagram)
 164  		if jsonErr != nil {
 165  			logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{
 166  				"payment_hash": transaction.PaymentHash,
 167  				"boostagram":   transaction.Boostagram,
 168  			}).Error("Failed to deserialize transaction boostagram info")
 169  		}
 170  		boostagram = toApiBoostagram(&txBoostagram)
 171  	}
 172  
 173  	return &Transaction{
 174  		ID:              transaction.ID,
 175  		Type:            transaction.Type,
 176  		State:           strings.ToLower(transaction.State),
 177  		Invoice:         transaction.PaymentRequest,
 178  		Description:     transaction.Description,
 179  		DescriptionHash: transaction.DescriptionHash,
 180  		Preimage:        preimage,
 181  		PaymentHash:     transaction.PaymentHash,
 182  		Amount:          transaction.AmountMsat,
 183  		AmountSat:       transaction.AmountMsat / 1000,
 184  		AmountMsat:      transaction.AmountMsat,
 185  		AppId:           transaction.AppId,
 186  		FeesPaid:        transaction.FeeMsat,
 187  		FeesPaidSat:     transaction.FeeMsat / 1000,
 188  		FeesPaidMsat:    transaction.FeeMsat,
 189  		UpdatedAt:       updatedAt,
 190  		CreatedAt:       createdAt,
 191  		SettledAt:       settledAt,
 192  		Metadata:        metadata,
 193  		Boostagram:      boostagram,
 194  		FailureReason:   transaction.FailureReason,
 195  	}
 196  }
 197  
 198  func (api *api) Transfer(ctx context.Context, fromAppId *uint, toAppId *uint, amountMsat uint64, description string) error {
 199  	lnClient := api.svc.GetLNClient()
 200  	if lnClient == nil {
 201  		return ErrLNClientNotStarted
 202  	}
 203  
 204  	for _, appId := range []*uint{fromAppId, toAppId} {
 205  		if appId != nil {
 206  			dbApp := api.appsSvc.GetAppById(*appId)
 207  			if dbApp == nil {
 208  				return errors.New("app does not exist")
 209  			}
 210  			if !dbApp.Isolated {
 211  				return errors.New("app is not isolated")
 212  			}
 213  		}
 214  	}
 215  
 216  	// default to "transfer"
 217  	if description == "" {
 218  		description = "transfer"
 219  	}
 220  
 221  	transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, toAppId, nil, nil)
 222  
 223  	if err != nil {
 224  		return err
 225  	}
 226  
 227  	_, err = api.svc.GetTransactionsService().SendPaymentSync(transaction.PaymentRequest, nil, nil, lnClient, fromAppId, nil)
 228  	return err
 229  }
 230  
 231  func toApiBoostagram(boostagram *transactions.Boostagram) *Boostagram {
 232  	return &Boostagram{
 233  		AppName:        boostagram.AppName,
 234  		Name:           boostagram.Name,
 235  		Podcast:        boostagram.Podcast,
 236  		URL:            boostagram.URL,
 237  		Episode:        boostagram.Episode.String(),
 238  		FeedId:         boostagram.FeedId.String(),
 239  		ItemId:         boostagram.ItemId.String(),
 240  		Timestamp:      boostagram.Timestamp,
 241  		Message:        boostagram.Message,
 242  		SenderId:       boostagram.SenderId.String(),
 243  		SenderName:     boostagram.SenderName,
 244  		Time:           boostagram.Time,
 245  		Action:         boostagram.Action,
 246  		ValueSatTotal:  boostagram.ValueMsatTotal / 1000,
 247  		ValueMsatTotal: boostagram.ValueMsatTotal,
 248  	}
 249  }
 250