package api import ( "context" "encoding/json" "errors" "fmt" "net/url" "strconv" "strings" "time" "github.com/getAlby/hub/constants" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/transactions" "github.com/sirupsen/logrus" ) func (api *api) CreateInvoice(ctx context.Context, amountMsat uint64, description string, toAppId *uint) (*MakeInvoiceResponse, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } if toAppId != nil && api.appsSvc.GetAppById(*toAppId) == nil { return nil, errors.New("app does not exist") } transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, toAppId, nil, nil) if err != nil { return nil, err } return toApiTransaction(transaction), nil } func (api *api) LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } transaction, err := api.svc.GetTransactionsService().LookupTransaction(ctx, paymentHash, nil, lnClient, nil) if err != nil { return nil, err } return toApiTransaction(transaction), nil } func (api *api) SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error { return api.svc.GetTransactionsService().SetTransactionUserLabels(ctx, id, labels) } // ParseListTransactionsFilters parses transaction filter query parameters // shared by the HTTP and Wails transports. Invalid values return an error. func ParseListTransactionsFilters(query url.Values) (ListTransactionsFilters, error) { filters := ListTransactionsFilters{} if transactionType := query.Get("type"); transactionType != "" { if transactionType != constants.TRANSACTION_TYPE_INCOMING && transactionType != constants.TRANSACTION_TYPE_OUTGOING { return filters, fmt.Errorf("invalid type: %s", transactionType) } filters.Type = &transactionType } if minAmountSatParam := query.Get("minAmountSat"); minAmountSatParam != "" { minAmountSat, err := strconv.ParseUint(minAmountSatParam, 10, 64) if err != nil || minAmountSat == 0 { return filters, fmt.Errorf("invalid minAmountSat: %s", minAmountSatParam) } const msatPerSat = uint64(1000) if minAmountSat > ^uint64(0)/msatPerSat { return filters, fmt.Errorf("minAmountSat is too large") } minAmountMsat := minAmountSat * msatPerSat filters.MinAmountMsat = &minAmountMsat } if hideFailedParam := query.Get("hideFailed"); hideFailedParam != "" { hideFailed, err := strconv.ParseBool(hideFailedParam) if err != nil { return filters, fmt.Errorf("invalid hideFailed: %s", hideFailedParam) } filters.HideFailed = hideFailed } filters.SearchTerm = strings.TrimSpace(query.Get("search")) return filters, nil } func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64, filters ListTransactionsFilters) (*ListTransactionsResponse, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } forceFilterByAppId := false if appId != nil { forceFilterByAppId = true } dbTransactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, lnClient, appId, forceFilterByAppId, &transactions.ListTransactionsFilters{ Type: filters.Type, MinAmountMsat: filters.MinAmountMsat, HideFailed: filters.HideFailed, SearchTerm: filters.SearchTerm, }) if err != nil { return nil, err } apiTransactions := []Transaction{} for _, transaction := range dbTransactions { apiTransactions = append(apiTransactions, *toApiTransaction(&transaction)) } return &ListTransactionsResponse{ Transactions: apiTransactions, TotalCount: totalCount, }, nil } func (api *api) SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}, appId *uint) (*SendPaymentResponse, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } transaction, err := api.svc.GetTransactionsService().SendPaymentSync(invoice, amountMsat, metadata, lnClient, appId, nil) if err != nil { return nil, err } return toApiTransaction(transaction), nil } func toApiTransaction(transaction *transactions.Transaction) *Transaction { updatedAt := transaction.UpdatedAt.Format(time.RFC3339) createdAt := transaction.CreatedAt.Format(time.RFC3339) var settledAt *string var preimage *string if transaction.SettledAt != nil { settledAtValue := transaction.SettledAt.Format(time.RFC3339) settledAt = &settledAtValue preimage = transaction.Preimage } var metadata Metadata if transaction.Metadata != nil { jsonErr := json.Unmarshal(transaction.Metadata, &metadata) if jsonErr != nil { logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{ "payment_hash": transaction.PaymentHash, "metadata": transaction.Metadata, }).Error("Failed to deserialize transaction metadata") } } var boostagram *Boostagram if transaction.Boostagram != nil { var txBoostagram transactions.Boostagram jsonErr := json.Unmarshal(transaction.Boostagram, &txBoostagram) if jsonErr != nil { logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{ "payment_hash": transaction.PaymentHash, "boostagram": transaction.Boostagram, }).Error("Failed to deserialize transaction boostagram info") } boostagram = toApiBoostagram(&txBoostagram) } return &Transaction{ ID: transaction.ID, Type: transaction.Type, State: strings.ToLower(transaction.State), Invoice: transaction.PaymentRequest, Description: transaction.Description, DescriptionHash: transaction.DescriptionHash, Preimage: preimage, PaymentHash: transaction.PaymentHash, Amount: transaction.AmountMsat, AmountSat: transaction.AmountMsat / 1000, AmountMsat: transaction.AmountMsat, AppId: transaction.AppId, FeesPaid: transaction.FeeMsat, FeesPaidSat: transaction.FeeMsat / 1000, FeesPaidMsat: transaction.FeeMsat, UpdatedAt: updatedAt, CreatedAt: createdAt, SettledAt: settledAt, Metadata: metadata, Boostagram: boostagram, FailureReason: transaction.FailureReason, } } func (api *api) Transfer(ctx context.Context, fromAppId *uint, toAppId *uint, amountMsat uint64, description string) error { lnClient := api.svc.GetLNClient() if lnClient == nil { return ErrLNClientNotStarted } for _, appId := range []*uint{fromAppId, toAppId} { if appId != nil { dbApp := api.appsSvc.GetAppById(*appId) if dbApp == nil { return errors.New("app does not exist") } if !dbApp.Isolated { return errors.New("app is not isolated") } } } // default to "transfer" if description == "" { description = "transfer" } transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, toAppId, nil, nil) if err != nil { return err } _, err = api.svc.GetTransactionsService().SendPaymentSync(transaction.PaymentRequest, nil, nil, lnClient, fromAppId, nil) return err } func toApiBoostagram(boostagram *transactions.Boostagram) *Boostagram { return &Boostagram{ AppName: boostagram.AppName, Name: boostagram.Name, Podcast: boostagram.Podcast, URL: boostagram.URL, Episode: boostagram.Episode.String(), FeedId: boostagram.FeedId.String(), ItemId: boostagram.ItemId.String(), Timestamp: boostagram.Timestamp, Message: boostagram.Message, SenderId: boostagram.SenderId.String(), SenderName: boostagram.SenderName, Time: boostagram.Time, Action: boostagram.Action, ValueSatTotal: boostagram.ValueMsatTotal / 1000, ValueMsatTotal: boostagram.ValueMsatTotal, } }