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