alby_service.go raw

   1  package alby
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"errors"
   7  	"fmt"
   8  	"io"
   9  	"net/http"
  10  	"time"
  11  
  12  	"github.com/getAlby/hub/logger"
  13  	"github.com/sirupsen/logrus"
  14  )
  15  
  16  const albyInternalAPIURL = "https://getalby.com/api"
  17  
  18  type albyService struct {
  19  }
  20  
  21  func NewAlbyService() *albyService {
  22  	return &albyService{}
  23  }
  24  
  25  func (svc *albyService) GetCurrencies(ctx context.Context) ([]Currency, error) {
  26  	client := &http.Client{Timeout: 10 * time.Second}
  27  	url := fmt.Sprintf("%s/rates", albyInternalAPIURL)
  28  
  29  	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
  30  	if err != nil {
  31  		logger.Logger.WithError(err).Error("Error creating request to currencies endpoint")
  32  		return nil, err
  33  	}
  34  	setDefaultRequestHeaders(req)
  35  
  36  	res, err := client.Do(req)
  37  	if err != nil {
  38  		logger.Logger.WithError(err).Error("Failed to fetch currencies from API")
  39  		return nil, err
  40  	}
  41  
  42  	defer res.Body.Close()
  43  
  44  	body, err := io.ReadAll(res.Body)
  45  	if err != nil {
  46  		logger.Logger.WithError(err).WithFields(logrus.Fields{
  47  			"url": url,
  48  		}).Error("Failed to read response body")
  49  		return nil, errors.New("failed to read response body")
  50  	}
  51  
  52  	if res.StatusCode >= 300 {
  53  		logger.Logger.WithFields(logrus.Fields{
  54  			"body":        string(body),
  55  			"status_code": res.StatusCode,
  56  		}).Error("Currencies endpoint returned non-success code")
  57  		return nil, fmt.Errorf("currencies endpoint returned non-success code: %s", string(body))
  58  	}
  59  
  60  	rawCurrencies := map[string]Currency{}
  61  	err = json.Unmarshal(body, &rawCurrencies)
  62  	if err != nil {
  63  		logger.Logger.WithFields(logrus.Fields{
  64  			"body":  string(body),
  65  			"error": err,
  66  		}).Error("Failed to decode currencies API response")
  67  		return nil, err
  68  	}
  69  
  70  	currencies := []Currency{}
  71  	for _, currency := range rawCurrencies {
  72  		currencies = append(currencies, currency)
  73  	}
  74  
  75  	return currencies, nil
  76  }
  77  
  78  func (svc *albyService) GetBitcoinRate(ctx context.Context, currency string) (*BitcoinRate, error) {
  79  	client := &http.Client{Timeout: 10 * time.Second}
  80  
  81  	url := fmt.Sprintf("%s/rates/%s", albyInternalAPIURL, currency)
  82  
  83  	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
  84  	if err != nil {
  85  		logger.Logger.WithFields(logrus.Fields{
  86  			"currency": currency,
  87  			"error":    err,
  88  		}).Error("Error creating request to Bitcoin rate endpoint")
  89  		return nil, err
  90  	}
  91  	setDefaultRequestHeaders(req)
  92  
  93  	res, err := client.Do(req)
  94  	if err != nil {
  95  		logger.Logger.WithFields(logrus.Fields{
  96  			"currency": currency,
  97  			"error":    err,
  98  		}).Error("Failed to fetch Bitcoin rate from API")
  99  		return nil, err
 100  	}
 101  
 102  	defer res.Body.Close()
 103  
 104  	body, err := io.ReadAll(res.Body)
 105  	if err != nil {
 106  		logger.Logger.WithError(err).WithFields(logrus.Fields{
 107  			"url": url,
 108  		}).Error("Failed to read response body")
 109  		return nil, errors.New("failed to read response body")
 110  	}
 111  
 112  	if res.StatusCode >= 300 {
 113  		logger.Logger.WithFields(logrus.Fields{
 114  			"currency":    currency,
 115  			"body":        string(body),
 116  			"status_code": res.StatusCode,
 117  		}).Error("Bitcoin rate endpoint returned non-success code")
 118  		return nil, fmt.Errorf("bitcoin rate endpoint returned non-success code: %s", string(body))
 119  	}
 120  
 121  	var rate = &BitcoinRate{}
 122  	err = json.Unmarshal(body, rate)
 123  	if err != nil {
 124  		logger.Logger.WithFields(logrus.Fields{
 125  			"currency": currency,
 126  			"body":     string(body),
 127  			"error":    err,
 128  		}).Error("Failed to decode Bitcoin rate API response")
 129  		return nil, err
 130  	}
 131  
 132  	return rate, nil
 133  }
 134  
 135  func (svc *albyService) GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) {
 136  	client := &http.Client{Timeout: 10 * time.Second}
 137  
 138  	req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/internal/channel_suggestions", albyInternalAPIURL), nil)
 139  	if err != nil {
 140  		logger.Logger.WithError(err).Error("Error creating request to channel_suggestions endpoint")
 141  		return nil, err
 142  	}
 143  
 144  	setDefaultRequestHeaders(req)
 145  
 146  	res, err := client.Do(req)
 147  	if err != nil {
 148  		logger.Logger.WithError(err).Error("Failed to fetch channel_suggestions endpoint")
 149  		return nil, err
 150  	}
 151  
 152  	body, err := io.ReadAll(res.Body)
 153  	if err != nil {
 154  		logger.Logger.WithError(err).Error("Failed to read response body")
 155  		return nil, errors.New("failed to read response body")
 156  	}
 157  
 158  	if res.StatusCode >= 300 {
 159  		logger.Logger.WithFields(logrus.Fields{
 160  			"body":        string(body),
 161  			"status_code": res.StatusCode,
 162  		}).Error("channel suggestions endpoint returned non-success code")
 163  		return nil, fmt.Errorf("channel suggestions endpoint returned non-success code: %s", string(body))
 164  	}
 165  
 166  	var suggestions []ChannelPeerSuggestion
 167  	err = json.Unmarshal(body, &suggestions)
 168  	if err != nil {
 169  		logger.Logger.WithError(err).Errorf("Failed to decode API response")
 170  		return nil, err
 171  	}
 172  
 173  	for i := range suggestions {
 174  		suggestions[i].MinimumChannelSizeSat = suggestions[i].MinimumChannelSize
 175  		suggestions[i].MaximumChannelSizeSat = suggestions[i].MaximumChannelSize
 176  	}
 177  
 178  	logger.Logger.WithFields(logrus.Fields{"channel_suggestions": suggestions}).Debug("Alby channel peer suggestions response")
 179  	return suggestions, nil
 180  }
 181  
 182  func (svc *albyService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
 183  	client := &http.Client{Timeout: 10 * time.Second}
 184  
 185  	req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/internal/info", albyInternalAPIURL), nil)
 186  	if err != nil {
 187  		logger.Logger.WithError(err).Error("Error creating request to alby info endpoint")
 188  		return nil, err
 189  	}
 190  
 191  	setDefaultRequestHeaders(req)
 192  
 193  	res, err := client.Do(req)
 194  	if err != nil {
 195  		logger.Logger.WithError(err).Error("Failed to fetch /info")
 196  		return nil, err
 197  	}
 198  
 199  	type albyInfoHub struct {
 200  		LatestVersion      string `json:"latest_version"`
 201  		LatestReleaseNotes string `json:"latest_release_notes"`
 202  	}
 203  
 204  	type albyInfoIncident struct {
 205  		Name    string `json:"name"`
 206  		Started string `json:"started"`
 207  		Status  string `json:"status"`
 208  		Impact  string `json:"impact"`
 209  		Url     string `json:"url"`
 210  	}
 211  
 212  	type albyInfo struct {
 213  		Hub              albyInfoHub        `json:"hub"`
 214  		Status           string             `json:"status"`
 215  		Healthy          bool               `json:"healthy"`
 216  		AccountAvailable bool               `json:"account_available"` // false if country is blocked (can still use Alby Hub without an Alby Account)
 217  		Incidents        []albyInfoIncident `json:"incidents"`
 218  	}
 219  
 220  	body, err := io.ReadAll(res.Body)
 221  	if err != nil {
 222  		logger.Logger.WithError(err).Error("Failed to read response body")
 223  		return nil, errors.New("failed to read response body")
 224  	}
 225  
 226  	if res.StatusCode >= 300 {
 227  		logger.Logger.WithFields(logrus.Fields{
 228  			"body":        string(body),
 229  			"status_code": res.StatusCode,
 230  		}).Error("info endpoint returned non-success code")
 231  		return nil, fmt.Errorf("info endpoint returned non-success code: %s", string(body))
 232  	}
 233  
 234  	info := &albyInfo{}
 235  	err = json.Unmarshal(body, info)
 236  	if err != nil {
 237  		logger.Logger.WithError(err).Error("Failed to decode API response")
 238  		return nil, err
 239  	}
 240  
 241  	incidents := []AlbyInfoIncident{}
 242  	for _, incident := range info.Incidents {
 243  		incidents = append(incidents, AlbyInfoIncident{
 244  			Name:    incident.Name,
 245  			Started: incident.Started,
 246  			Status:  incident.Status,
 247  			Impact:  incident.Impact,
 248  			Url:     incident.Url,
 249  		})
 250  	}
 251  
 252  	return &AlbyInfo{
 253  		Hub: AlbyInfoHub{
 254  			LatestVersion:      info.Hub.LatestVersion,
 255  			LatestReleaseNotes: info.Hub.LatestReleaseNotes,
 256  		},
 257  		Status:           info.Status,
 258  		Healthy:          info.Healthy,
 259  		AccountAvailable: info.AccountAvailable,
 260  		Incidents:        incidents,
 261  	}, nil
 262  }
 263