alby-hub/api/rebalance.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

147 lines
4.8 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/version"
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/sirupsen/logrus"
)
func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *RebalanceChannelRequest) (*RebalanceChannelResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
receiveMetadata := map[string]interface{}{
"receive_through": rebalanceChannelRequest.ReceiveThroughNodePubkey,
}
receiveInvoice, err := api.svc.GetTransactionsService().MakeInvoice(ctx, rebalanceChannelRequest.AmountSat*1000, "Alby Hub Rebalance through "+rebalanceChannelRequest.ReceiveThroughNodePubkey, "", 0, receiveMetadata, lnClient, nil, nil, &rebalanceChannelRequest.ReceiveThroughNodePubkey)
if err != nil {
logger.Logger.WithError(err).Error("failed to generate rebalance receive invoice")
return nil, err
}
type rspCreateOrderRequest struct {
Token string `json:"token"`
PayRequest string `json:"pay_request"`
PayThroughThisPublicKey string `json:"pay_through_this_public_key"`
}
newRspCreateOrderRequest := rspCreateOrderRequest{
Token: "alby-hub",
PayRequest: receiveInvoice.PaymentRequest,
PayThroughThisPublicKey: rebalanceChannelRequest.ReceiveThroughNodePubkey,
}
payloadBytes, err := json.Marshal(newRspCreateOrderRequest)
if err != nil {
return nil, err
}
bodyReader := bytes.NewReader(payloadBytes)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, api.cfg.GetEnv().RebalanceServiceUrl+"/api/rebalance/v1/create_order", bodyReader)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"request": newRspCreateOrderRequest,
}).Error("Failed to create new rebalance request")
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "AlbyHub/"+version.Tag)
client := http.Client{
Timeout: time.Second * 60,
}
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"request": newRspCreateOrderRequest,
}).Error("Failed to request new rebalance order")
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"request": newRspCreateOrderRequest,
}).Error("Failed to read response body")
return nil, errors.New("failed to read response body")
}
if res.StatusCode != http.StatusOK {
logger.Logger.WithFields(logrus.Fields{
"request": newRspCreateOrderRequest,
"body": string(body),
"statusCode": res.StatusCode,
}).Error("rebalance create_order endpoint returned non-success code")
return nil, fmt.Errorf("rebalance create_order endpoint returned non-success code: %s", string(body))
}
type rspRebalanceCreateOrderResponse struct {
OrderId string `json:"order_id"`
PayRequest string `json:"pay_request"`
}
var rebalanceCreateOrderResponse rspRebalanceCreateOrderResponse
err = json.Unmarshal(body, &rebalanceCreateOrderResponse)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"request": newRspCreateOrderRequest,
}).Error("Failed to deserialize json")
return nil, fmt.Errorf("failed to deserialize json from rebalance create order response: %s", string(body))
}
logger.Logger.WithField("response", rebalanceCreateOrderResponse).Info("New rebalance order created")
paymentRequest, err := decodepay.Decodepay(rebalanceCreateOrderResponse.PayRequest)
if err != nil {
logger.Logger.WithError(err).Error("Failed to decode bolt11 invoice")
return nil, err
}
if paymentRequest.MSatoshi > int64(float64(rebalanceChannelRequest.AmountSat)*float64(1000)*float64(1.003)+1 /*0.3% fees*/) {
return nil, errors.New("rebalance payment is more expensive than expected")
}
payMetadata := map[string]interface{}{
"receive_through": rebalanceChannelRequest.ReceiveThroughNodePubkey,
"amount_sat": rebalanceChannelRequest.AmountSat,
"order_id": rebalanceCreateOrderResponse.OrderId,
}
payRebalanceInvoiceResponse, err := api.svc.GetTransactionsService().SendPaymentSync(rebalanceCreateOrderResponse.PayRequest, nil, payMetadata, lnClient, nil, nil)
if err != nil {
logger.Logger.WithError(err).Error("failed to pay rebalance invoice")
return nil, err
}
api.eventPublisher.Publish(&events.Event{
Event: "nwc_rebalance_succeeded",
Properties: map[string]interface{}{},
})
totalFeeMsat := uint64(paymentRequest.MSatoshi) + payRebalanceInvoiceResponse.FeeMsat - rebalanceChannelRequest.AmountSat*1000
return &RebalanceChannelResponse{
TotalFeeSat: totalFeeMsat / 1000,
TotalFeeMsat: totalFeeMsat,
}, nil
}