alby-hub/api/transactions.go
Anshuman 1d3f9ecd41
feat: suffix balance/amount fields with explicit unit (Sat or Msat) (#2153)
* feat: add both Sat and Msat companion fields for all ambiguous balance/amount properties

* feat: add sat and msat companion fields to backend API responses

* chore: align frontend types and other missing fields

* chore: keep old formula for calculating total fee sat

* chore: remove unnecessary balance assignments in cashu and phoenix

* chore: add deprecated comment to non unit fields

* chore: rename callers and variable to specify units

* chore: remove msat fields for channel size and liquidity fields

* chore: drop msat fields from onchain channel size and liquidity responses

* chore: further changes

* chore: remove amount msat field onchain tx

* chore: remove msats from onchain balance response

* chore: remove msat fields for swaps

* chore: remove msat fields for punishment reserves

* chore: simplify rebalancing fee calculation

* chore: remove unnecessary fields

* chore: mark deprecated fields in frontend types

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-17 13:53:28 +05:30

190 lines
5.8 KiB
Go

package api
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"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) (*MakeInvoiceResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, nil, 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) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
forceFilterByAppId := false
if appId != nil {
forceFilterByAppId = true
}
transactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, nil, lnClient, appId, forceFilterByAppId)
if err != nil {
return nil, err
}
apiTransactions := []Transaction{}
for _, transaction := range transactions {
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{}) (*SendPaymentResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
transaction, err := api.svc.GetTransactionsService().SendPaymentSync(invoice, amountMsat, metadata, lnClient, nil, 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{
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,
}
}