rebalance.go raw
1 package api
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/http"
11 "time"
12
13 "github.com/getAlby/hub/events"
14 "github.com/getAlby/hub/logger"
15 "github.com/getAlby/hub/version"
16 decodepay "github.com/nbd-wtf/ln-decodepay"
17 "github.com/sirupsen/logrus"
18 )
19
20 func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *RebalanceChannelRequest) (*RebalanceChannelResponse, error) {
21 lnClient := api.svc.GetLNClient()
22 if lnClient == nil {
23 return nil, ErrLNClientNotStarted
24 }
25
26 amountSat := uint64(0)
27 resolvedAmountSat := ResolveToSat(rebalanceChannelRequest.AmountSat, rebalanceChannelRequest.AmountMsat, nil, nil)
28 if resolvedAmountSat != nil {
29 amountSat = *resolvedAmountSat
30 }
31
32 receiveMetadata := map[string]interface{}{
33 "receive_through": rebalanceChannelRequest.ReceiveThroughNodePubkey,
34 }
35
36 receiveInvoice, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountSat*1000, "Alby Hub Rebalance through "+rebalanceChannelRequest.ReceiveThroughNodePubkey, "", 0, receiveMetadata, lnClient, nil, nil, &rebalanceChannelRequest.ReceiveThroughNodePubkey)
37 if err != nil {
38 logger.Logger.WithError(err).Error("failed to generate rebalance receive invoice")
39 return nil, err
40 }
41
42 type rspCreateOrderRequest struct {
43 Token string `json:"token"`
44 PayRequest string `json:"pay_request"`
45 PayThroughThisPublicKey string `json:"pay_through_this_public_key"`
46 }
47
48 newRspCreateOrderRequest := rspCreateOrderRequest{
49 Token: "alby-hub",
50 PayRequest: receiveInvoice.PaymentRequest,
51 PayThroughThisPublicKey: rebalanceChannelRequest.ReceiveThroughNodePubkey,
52 }
53
54 payloadBytes, err := json.Marshal(newRspCreateOrderRequest)
55 if err != nil {
56 return nil, err
57 }
58 bodyReader := bytes.NewReader(payloadBytes)
59
60 req, err := http.NewRequestWithContext(ctx, http.MethodPost, api.cfg.GetEnv().RebalanceServiceUrl+"/api/rebalance/v1/create_order", bodyReader)
61 if err != nil {
62 logger.Logger.WithError(err).WithFields(logrus.Fields{
63 "request": newRspCreateOrderRequest,
64 }).Error("Failed to create new rebalance request")
65 return nil, err
66 }
67
68 req.Header.Set("Content-Type", "application/json")
69 req.Header.Set("User-Agent", "AlbyHub/"+version.Tag)
70
71 client := http.Client{
72 Timeout: time.Second * 60,
73 }
74
75 res, err := client.Do(req)
76 if err != nil {
77 logger.Logger.WithError(err).WithFields(logrus.Fields{
78 "request": newRspCreateOrderRequest,
79 }).Error("Failed to request new rebalance order")
80 return nil, err
81 }
82
83 defer res.Body.Close()
84
85 body, err := io.ReadAll(res.Body)
86 if err != nil {
87 logger.Logger.WithError(err).WithFields(logrus.Fields{
88 "request": newRspCreateOrderRequest,
89 }).Error("Failed to read response body")
90 return nil, errors.New("failed to read response body")
91 }
92
93 if res.StatusCode != http.StatusOK {
94 logger.Logger.WithFields(logrus.Fields{
95 "request": newRspCreateOrderRequest,
96 "body": string(body),
97 "statusCode": res.StatusCode,
98 }).Error("rebalance create_order endpoint returned non-success code")
99 return nil, fmt.Errorf("rebalance create_order endpoint returned non-success code: %s", string(body))
100 }
101
102 type rspRebalanceCreateOrderResponse struct {
103 OrderId string `json:"order_id"`
104 PayRequest string `json:"pay_request"`
105 }
106
107 var rebalanceCreateOrderResponse rspRebalanceCreateOrderResponse
108
109 err = json.Unmarshal(body, &rebalanceCreateOrderResponse)
110 if err != nil {
111 logger.Logger.WithError(err).WithFields(logrus.Fields{
112 "request": newRspCreateOrderRequest,
113 }).Error("Failed to deserialize json")
114 return nil, fmt.Errorf("failed to deserialize json from rebalance create order response: %s", string(body))
115 }
116
117 logger.Logger.WithField("response", rebalanceCreateOrderResponse).Info("New rebalance order created")
118
119 paymentRequest, err := decodepay.Decodepay(rebalanceCreateOrderResponse.PayRequest)
120 if err != nil {
121 logger.Logger.WithError(err).Error("Failed to decode bolt11 invoice")
122 return nil, err
123 }
124
125 if paymentRequest.MSatoshi > int64(float64(amountSat)*float64(1000)*float64(1.005)+1 /*0.5% fees*/) {
126 return nil, errors.New("rebalance payment is more expensive than expected")
127 }
128
129 payMetadata := map[string]interface{}{
130 "receive_through": rebalanceChannelRequest.ReceiveThroughNodePubkey,
131 "amount_sat": amountSat,
132 "order_id": rebalanceCreateOrderResponse.OrderId,
133 }
134
135 payRebalanceInvoiceResponse, err := api.svc.GetTransactionsService().SendPaymentSync(rebalanceCreateOrderResponse.PayRequest, nil, payMetadata, lnClient, nil, nil)
136
137 if err != nil {
138 logger.Logger.WithError(err).Error("failed to pay rebalance invoice")
139 return nil, err
140 }
141
142 api.eventPublisher.Publish(&events.Event{
143 Event: "nwc_rebalance_succeeded",
144 Properties: map[string]interface{}{},
145 })
146
147 totalFeeMsat := uint64(paymentRequest.MSatoshi) + payRebalanceInvoiceResponse.FeeMsat - amountSat*1000
148
149 return &RebalanceChannelResponse{
150 TotalFeeSat: totalFeeMsat / 1000,
151 TotalFeeMsat: totalFeeMsat,
152 }, nil
153 }
154