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