transactions.go raw
1 package api
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "net/url"
9 "strconv"
10 "strings"
11 "time"
12
13 "github.com/getAlby/hub/constants"
14 "github.com/getAlby/hub/logger"
15 "github.com/getAlby/hub/transactions"
16 "github.com/sirupsen/logrus"
17 )
18
19 func (api *api) CreateInvoice(ctx context.Context, amountMsat uint64, description string, toAppId *uint) (*MakeInvoiceResponse, error) {
20 lnClient := api.svc.GetLNClient()
21 if lnClient == nil {
22 return nil, ErrLNClientNotStarted
23 }
24
25 if toAppId != nil && api.appsSvc.GetAppById(*toAppId) == nil {
26 return nil, errors.New("app does not exist")
27 }
28
29 transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, toAppId, nil, nil)
30 if err != nil {
31 return nil, err
32 }
33 return toApiTransaction(transaction), nil
34 }
35
36 func (api *api) LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error) {
37 lnClient := api.svc.GetLNClient()
38 if lnClient == nil {
39 return nil, ErrLNClientNotStarted
40 }
41 transaction, err := api.svc.GetTransactionsService().LookupTransaction(ctx, paymentHash, nil, lnClient, nil)
42 if err != nil {
43 return nil, err
44 }
45 return toApiTransaction(transaction), nil
46 }
47
48 func (api *api) SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error {
49 return api.svc.GetTransactionsService().SetTransactionUserLabels(ctx, id, labels)
50 }
51
52 // ParseListTransactionsFilters parses transaction filter query parameters
53 // shared by the HTTP and Wails transports. Invalid values return an error.
54 func ParseListTransactionsFilters(query url.Values) (ListTransactionsFilters, error) {
55 filters := ListTransactionsFilters{}
56
57 if transactionType := query.Get("type"); transactionType != "" {
58 if transactionType != constants.TRANSACTION_TYPE_INCOMING && transactionType != constants.TRANSACTION_TYPE_OUTGOING {
59 return filters, fmt.Errorf("invalid type: %s", transactionType)
60 }
61 filters.Type = &transactionType
62 }
63
64 if minAmountSatParam := query.Get("minAmountSat"); minAmountSatParam != "" {
65 minAmountSat, err := strconv.ParseUint(minAmountSatParam, 10, 64)
66 if err != nil || minAmountSat == 0 {
67 return filters, fmt.Errorf("invalid minAmountSat: %s", minAmountSatParam)
68 }
69
70 const msatPerSat = uint64(1000)
71 if minAmountSat > ^uint64(0)/msatPerSat {
72 return filters, fmt.Errorf("minAmountSat is too large")
73 }
74
75 minAmountMsat := minAmountSat * msatPerSat
76 filters.MinAmountMsat = &minAmountMsat
77 }
78
79 if hideFailedParam := query.Get("hideFailed"); hideFailedParam != "" {
80 hideFailed, err := strconv.ParseBool(hideFailedParam)
81 if err != nil {
82 return filters, fmt.Errorf("invalid hideFailed: %s", hideFailedParam)
83 }
84 filters.HideFailed = hideFailed
85 }
86
87 filters.SearchTerm = strings.TrimSpace(query.Get("search"))
88
89 return filters, nil
90 }
91
92 func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64, filters ListTransactionsFilters) (*ListTransactionsResponse, error) {
93 lnClient := api.svc.GetLNClient()
94 if lnClient == nil {
95 return nil, ErrLNClientNotStarted
96 }
97
98 forceFilterByAppId := false
99 if appId != nil {
100 forceFilterByAppId = true
101 }
102
103 dbTransactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, lnClient, appId, forceFilterByAppId, &transactions.ListTransactionsFilters{
104 Type: filters.Type,
105 MinAmountMsat: filters.MinAmountMsat,
106 HideFailed: filters.HideFailed,
107 SearchTerm: filters.SearchTerm,
108 })
109 if err != nil {
110 return nil, err
111 }
112
113 apiTransactions := []Transaction{}
114 for _, transaction := range dbTransactions {
115 apiTransactions = append(apiTransactions, *toApiTransaction(&transaction))
116 }
117
118 return &ListTransactionsResponse{
119 Transactions: apiTransactions,
120 TotalCount: totalCount,
121 }, nil
122 }
123
124 func (api *api) SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}, appId *uint) (*SendPaymentResponse, error) {
125 lnClient := api.svc.GetLNClient()
126 if lnClient == nil {
127 return nil, ErrLNClientNotStarted
128 }
129
130 transaction, err := api.svc.GetTransactionsService().SendPaymentSync(invoice, amountMsat, metadata, lnClient, appId, nil)
131 if err != nil {
132 return nil, err
133 }
134 return toApiTransaction(transaction), nil
135 }
136
137 func toApiTransaction(transaction *transactions.Transaction) *Transaction {
138
139 updatedAt := transaction.UpdatedAt.Format(time.RFC3339)
140 createdAt := transaction.CreatedAt.Format(time.RFC3339)
141 var settledAt *string
142 var preimage *string
143 if transaction.SettledAt != nil {
144 settledAtValue := transaction.SettledAt.Format(time.RFC3339)
145 settledAt = &settledAtValue
146 preimage = transaction.Preimage
147 }
148
149 var metadata Metadata
150 if transaction.Metadata != nil {
151 jsonErr := json.Unmarshal(transaction.Metadata, &metadata)
152 if jsonErr != nil {
153 logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{
154 "payment_hash": transaction.PaymentHash,
155 "metadata": transaction.Metadata,
156 }).Error("Failed to deserialize transaction metadata")
157 }
158 }
159
160 var boostagram *Boostagram
161 if transaction.Boostagram != nil {
162 var txBoostagram transactions.Boostagram
163 jsonErr := json.Unmarshal(transaction.Boostagram, &txBoostagram)
164 if jsonErr != nil {
165 logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{
166 "payment_hash": transaction.PaymentHash,
167 "boostagram": transaction.Boostagram,
168 }).Error("Failed to deserialize transaction boostagram info")
169 }
170 boostagram = toApiBoostagram(&txBoostagram)
171 }
172
173 return &Transaction{
174 ID: transaction.ID,
175 Type: transaction.Type,
176 State: strings.ToLower(transaction.State),
177 Invoice: transaction.PaymentRequest,
178 Description: transaction.Description,
179 DescriptionHash: transaction.DescriptionHash,
180 Preimage: preimage,
181 PaymentHash: transaction.PaymentHash,
182 Amount: transaction.AmountMsat,
183 AmountSat: transaction.AmountMsat / 1000,
184 AmountMsat: transaction.AmountMsat,
185 AppId: transaction.AppId,
186 FeesPaid: transaction.FeeMsat,
187 FeesPaidSat: transaction.FeeMsat / 1000,
188 FeesPaidMsat: transaction.FeeMsat,
189 UpdatedAt: updatedAt,
190 CreatedAt: createdAt,
191 SettledAt: settledAt,
192 Metadata: metadata,
193 Boostagram: boostagram,
194 FailureReason: transaction.FailureReason,
195 }
196 }
197
198 func (api *api) Transfer(ctx context.Context, fromAppId *uint, toAppId *uint, amountMsat uint64, description string) error {
199 lnClient := api.svc.GetLNClient()
200 if lnClient == nil {
201 return ErrLNClientNotStarted
202 }
203
204 for _, appId := range []*uint{fromAppId, toAppId} {
205 if appId != nil {
206 dbApp := api.appsSvc.GetAppById(*appId)
207 if dbApp == nil {
208 return errors.New("app does not exist")
209 }
210 if !dbApp.Isolated {
211 return errors.New("app is not isolated")
212 }
213 }
214 }
215
216 // default to "transfer"
217 if description == "" {
218 description = "transfer"
219 }
220
221 transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, toAppId, nil, nil)
222
223 if err != nil {
224 return err
225 }
226
227 _, err = api.svc.GetTransactionsService().SendPaymentSync(transaction.PaymentRequest, nil, nil, lnClient, fromAppId, nil)
228 return err
229 }
230
231 func toApiBoostagram(boostagram *transactions.Boostagram) *Boostagram {
232 return &Boostagram{
233 AppName: boostagram.AppName,
234 Name: boostagram.Name,
235 Podcast: boostagram.Podcast,
236 URL: boostagram.URL,
237 Episode: boostagram.Episode.String(),
238 FeedId: boostagram.FeedId.String(),
239 ItemId: boostagram.ItemId.String(),
240 Timestamp: boostagram.Timestamp,
241 Message: boostagram.Message,
242 SenderId: boostagram.SenderId.String(),
243 SenderName: boostagram.SenderName,
244 Time: boostagram.Time,
245 Action: boostagram.Action,
246 ValueSatTotal: boostagram.ValueMsatTotal / 1000,
247 ValueMsatTotal: boostagram.ValueMsatTotal,
248 }
249 }
250