-
Notifications
You must be signed in to change notification settings - Fork 1
/
payment.go
295 lines (249 loc) · 7.63 KB
/
payment.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package app
import (
"context"
"fmt"
"strings"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/c13n-io/c13n-go/lnchat"
"github.com/c13n-io/c13n-go/model"
"github.com/c13n-io/c13n-go/store"
)
// SendMessage attempts to send a message.
// If a payment request is provided, a discussion with the recipient
// is created with default options if it does not exist.
// Note: Anonymous messages to group discussions are disallowed.
func (app *App) SendMessage(ctx context.Context, discID uint64, amtMsat int64, payReq string,
payload string, opts model.MessageOptions) (*model.MessageAggregate, error) {
// Validate arguments
if (payReq != "") && (discID != 0) {
return nil, fmt.Errorf("exactly one of payment request " +
"and discussion must be specified")
}
// Retrieve discussion
var err error
var disc *model.Discussion
switch payReq {
default:
// Create discussion if it does not exist
payRequest, err := app.LNManager.DecodePayReq(ctx, payReq)
if err != nil {
return nil, errors.Wrap(err, "could not decode payment request")
}
disc, err = app.retrieveOrCreateDiscussion(&model.Discussion{
Participants: []string{payRequest.Destination.String()},
Options: DefaultOptions,
})
if err != nil {
return nil, errors.Wrap(err, "could not retrieve discussion")
}
case "":
disc, err = app.retrieveDiscussion(ctx, discID)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve discussion")
}
}
// Disallow anonymous messages in group discussions
options := overrideOptions(disc.Options, true, opts)
if len(disc.Participants) > 1 && options.Anonymous {
return nil, ErrDiscAnonymousMessage
}
payOpts := options.GetPaymentOptions()
// Create raw message
rawMsg, err := app.createRawMessage(ctx, disc, payload, !options.Anonymous)
if err != nil {
return nil, err
}
tlvs := marshalPayload(rawMsg)
// Perform payment attempts in parallel
var wg sync.WaitGroup
var results sync.Map
for _, recipient := range disc.Participants {
wg.Add(1)
go func(dest string) {
defer wg.Done()
result, err := app.send(ctx, dest, amtMsat, payReq, payOpts, tlvs)
if err != nil {
results.Store(dest, err)
return
}
results.Store(dest, result)
}(recipient)
}
wg.Wait()
// Aggregate payments and errors
var errs []error
payments := make([]*model.Payment, 0, len(disc.Participants))
results.Range(func(key, val interface{}) bool {
recipient, ok := key.(string)
if !ok {
return false
}
switch v := val.(type) {
case lnchat.PaymentUpdate:
if v.Err != nil {
errs = append(errs, fmt.Errorf("payment error "+
"for recipient %s: %w", recipient, v.Err))
break
}
payments = append(payments, &model.Payment{
PayerAddress: app.Self.Node.Address,
PayeeAddress: recipient,
Payment: *v.Payment,
})
case error:
errs = append(errs, fmt.Errorf("could not initiate "+
"payment to recipient %s: %w", recipient, err))
}
return true
})
// If there are no payments associated with the message, fail early
if len(payments) == 0 {
return nil, newCompositeError(errs)
}
// Store payments and associate them with the message
for _, payment := range payments {
if err := app.Database.AddPayments(payment); err != nil {
// Ignore error if it indicates payment is already stored
var existsErr *store.AlreadyExistsError
if !errors.As(err, &existsErr) {
errs = append(errs, err)
}
}
rawMsg.WithPaymentIndexes(payment.PaymentIndex)
}
msg := model.MessageAggregate{
RawMessage: rawMsg,
Payments: payments,
}
// Store and publish raw message
if err := app.Database.AddRawMessage(msg.RawMessage); err != nil {
return &msg, errors.Wrap(err, "could not store message")
}
if err := app.publishMessage(msg); err != nil {
return &msg, errors.Wrap(err, "message notification failed")
}
return &msg, newCompositeError(errs)
}
// defaultPaymentFilter is a payment update filter,
// accepting only final payment updates.
func defaultPaymentFilter(p *lnchat.Payment) bool {
return p.Status == lnchat.PaymentSUCCEEDED ||
p.Status == lnchat.PaymentFAILED
}
// SendPayment attempts to send a payment.
func (app *App) SendPayment(ctx context.Context,
dest string, amtMsat int64, payReq string,
opts lnchat.PaymentOptions, tlvs map[uint64][]byte) (*model.Payment, error) {
// Validate arguments
if (payReq != "") == (dest != "") {
return nil, fmt.Errorf("exactly one of payment request " +
"and destination address must be specified")
}
recipient := dest
if payReq != "" {
decodedPayReq, err := app.LNManager.DecodePayReq(ctx, payReq)
if err != nil {
return nil, err
}
recipient = decodedPayReq.Destination.String()
}
// Perform payment attempt
result, err := app.send(ctx, dest, amtMsat, payReq, opts, tlvs)
if err != nil {
return nil, err
}
if result.Err != nil {
return nil, result.Err
}
payment := &model.Payment{
PayerAddress: app.Self.Node.Address,
PayeeAddress: recipient,
Payment: *result.Payment,
}
// Store payment
if err := app.Database.AddPayments(payment); err != nil {
var existsErr *store.AlreadyExistsError
if !errors.As(err, &existsErr) {
return payment, err
}
}
return payment, nil
}
// Attempts payment and returns the final payment update.
func (app *App) send(ctx context.Context, dest string, amtMsat int64, payReq string,
opts lnchat.PaymentOptions, tlvs map[uint64][]byte) (lnchat.PaymentUpdate, error) {
var lastUpdate lnchat.PaymentUpdate
amt := lnchat.NewAmount(amtMsat)
updates, err := app.LNManager.SendPayment(ctx, dest, amt, payReq,
opts, tlvs, defaultPaymentFilter)
if err != nil {
return lastUpdate, err
}
for update := range updates {
lastUpdate = update
}
return lastUpdate, nil
}
// GetPayments retrieves stored payments.
func (app *App) GetPayments(_ context.Context, pageOpts model.PageOptions) ([]*model.Payment, error) {
payments, err := app.Database.GetPayments(pageOpts)
if err != nil {
return nil, newErrorf(err, "could not retrieve payments")
}
return payments, nil
}
// SendPay attempts to send a payment.
// A payment fulfils the payment request, if one is provided,
// otherwise it is addressed to the discussion participants.
// If payload is present, it is sent along with the payment
// (respecting the provided and discussion options, if applicable).
func (app *App) SendPay(ctx context.Context,
payload string, amtMsat int64, discID uint64, payReq string,
opts model.MessageOptions) (*model.Message, error) {
msgAgg, err := app.SendMessage(ctx,
discID, amtMsat, payReq, payload, opts)
if err != nil {
return nil, err
}
msg, err := model.NewOutgoingMessage(msgAgg.RawMessage, true, msgAgg.Payments...)
if err != nil {
return nil, errors.Wrap(err, "message marshalling failed")
}
return msg, nil
}
func (app *App) createRawMessage(ctx context.Context, discussion *model.Discussion,
payload string, withSig bool) (*model.RawMessage, error) {
rawMsg, err := model.NewRawMessage(discussion, payload)
if err != nil {
return nil, errors.Wrap(err, "could not create raw message")
}
if withSig {
sig, err := app.LNManager.SignMessage(ctx, rawMsg.RawPayload)
if err != nil {
return nil, errors.Wrap(err, "could not sign message payload")
}
err = rawMsg.WithSignature(app.Self.Node.Address, sig)
if err != nil {
return nil, errors.Wrap(err, "could not add signature to raw message")
}
}
return rawMsg, nil
}
func newCompositeError(es []error) error {
if len(es) == 0 {
return nil
}
err := &multierror.Error{
ErrorFormat: func(errs []error) string {
es := make([]string, len(errs))
for i, e := range errs {
es[i] = e.Error()
}
return strings.Join(es, "; ")
},
Errors: es,
}
return err
}