-
Notifications
You must be signed in to change notification settings - Fork 45
/
Copy pathinvoices_client.go
417 lines (344 loc) · 10.8 KB
/
invoices_client.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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
package lndclient
import (
"context"
"errors"
"fmt"
"io"
"sync"
"time"
"github.com/btcsuite/btcd/btcutil"
invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"google.golang.org/grpc"
)
// InvoiceHtlcModifyRequest is a request to modify an HTLC that is attempting to
// settle an invoice.
type InvoiceHtlcModifyRequest struct {
// Invoice is the current state of the invoice, _before_ this HTLC is
// applied. Any HTLC in the invoice is a previously accepted/settled
// one.
Invoice *lnrpc.Invoice
// CircuitKey is the circuit key of the HTLC that is attempting to
// settle the invoice.
CircuitKey invpkg.CircuitKey
// ExitHtlcAmt is the amount of the HTLC that is attempting to settle
// the invoice.
ExitHtlcAmt lnwire.MilliSatoshi
// ExitHtlcExpiry is the expiry of the HTLC that is attempting to settle
// the invoice.
ExitHtlcExpiry uint32
// CurrentHeight is the current block height.
CurrentHeight uint32
// WireCustomRecords is the wire custom records of the HTLC that is
// attempting to settle the invoice.
WireCustomRecords lnwire.CustomRecords
}
// InvoiceHtlcModifyResponse is a response to an HTLC modification request.
type InvoiceHtlcModifyResponse struct {
// CircuitKey is the circuit key the response is for.
CircuitKey invpkg.CircuitKey
// AmtPaid is the amount the HTLC contributes toward settling the
// invoice. This amount can be different from the on-chain amount of the
// HTLC in case of custom channels. To not modify the amount and use the
// on-chain amount, set this to 0.
AmtPaid lnwire.MilliSatoshi
// CancelSet is a flag that indicates whether the HTLCs associated with
// the invoice should get cancelled.
CancelSet bool
}
// InvoiceHtlcModifyHandler is a function that handles an HTLC modification
// request.
type InvoiceHtlcModifyHandler func(context.Context,
InvoiceHtlcModifyRequest) (*InvoiceHtlcModifyResponse, error)
// InvoicesClient exposes invoice functionality.
type InvoicesClient interface {
ServiceClient[invoicesrpc.InvoicesClient]
SubscribeSingleInvoice(ctx context.Context, hash lntypes.Hash) (
<-chan InvoiceUpdate, <-chan error, error)
SettleInvoice(ctx context.Context, preimage lntypes.Preimage) error
CancelInvoice(ctx context.Context, hash lntypes.Hash) error
AddHoldInvoice(ctx context.Context, in *invoicesrpc.AddInvoiceData) (
string, error)
// HtlcModifier is a bidirectional streaming RPC that allows a client to
// intercept and modify the HTLCs that attempt to settle the given
// invoice. The server will send HTLCs of invoices to the client and the
// client can modify some aspects of the HTLC in order to pass the
// invoice acceptance tests.
HtlcModifier(ctx context.Context,
handler InvoiceHtlcModifyHandler) error
}
// InvoiceUpdate contains a state update for an invoice.
type InvoiceUpdate struct {
State invpkg.ContractState
AmtPaid btcutil.Amount
}
type invoicesClient struct {
client invoicesrpc.InvoicesClient
invoiceMac serializedMacaroon
timeout time.Duration
quitOnce sync.Once
quit chan struct{}
wg sync.WaitGroup
}
// A compile time check to ensure that invoicesClient implements the
// InvoicesClient interface.
var _ InvoicesClient = (*invoicesClient)(nil)
func newInvoicesClient(conn grpc.ClientConnInterface,
invoiceMac serializedMacaroon, timeout time.Duration) *invoicesClient {
return &invoicesClient{
client: invoicesrpc.NewInvoicesClient(conn),
invoiceMac: invoiceMac,
timeout: timeout,
quit: make(chan struct{}),
}
}
func (s *invoicesClient) WaitForFinished() {
s.quitOnce.Do(func() {
close(s.quit)
})
s.wg.Wait()
}
// RawClientWithMacAuth returns a context with the proper macaroon
// authentication, the default RPC timeout, and the raw client.
func (s *invoicesClient) RawClientWithMacAuth(
parentCtx context.Context) (context.Context, time.Duration,
invoicesrpc.InvoicesClient) {
return s.invoiceMac.WithMacaroonAuth(parentCtx), s.timeout, s.client
}
func (s *invoicesClient) SettleInvoice(ctx context.Context,
preimage lntypes.Preimage) error {
timeoutCtx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
rpcCtx := s.invoiceMac.WithMacaroonAuth(timeoutCtx)
_, err := s.client.SettleInvoice(rpcCtx, &invoicesrpc.SettleInvoiceMsg{
Preimage: preimage[:],
})
return err
}
func (s *invoicesClient) CancelInvoice(ctx context.Context,
hash lntypes.Hash) error {
rpcCtx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
rpcCtx = s.invoiceMac.WithMacaroonAuth(rpcCtx)
_, err := s.client.CancelInvoice(rpcCtx, &invoicesrpc.CancelInvoiceMsg{
PaymentHash: hash[:],
})
return err
}
func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
hash lntypes.Hash) (<-chan InvoiceUpdate,
<-chan error, error) {
invoiceStream, err := s.client.SubscribeSingleInvoice(
s.invoiceMac.WithMacaroonAuth(ctx),
&invoicesrpc.SubscribeSingleInvoiceRequest{
RHash: hash[:],
},
)
if err != nil {
return nil, nil, err
}
updateChan := make(chan InvoiceUpdate)
errChan := make(chan error, 1)
// Invoice updates goroutine.
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
invoice, err := invoiceStream.Recv()
if err != nil {
// If we get an EOF error, the invoice has
// reached a final state and the server is
// finished sending us updates. We close both
// channels to signal that we are done sending
// values on them and return.
if err == io.EOF {
close(updateChan)
close(errChan)
return
}
errChan <- err
return
}
state, err := fromRPCInvoiceState(invoice.State)
if err != nil {
errChan <- err
return
}
select {
case updateChan <- InvoiceUpdate{
State: state,
AmtPaid: btcutil.Amount(invoice.AmtPaidSat),
}:
case <-ctx.Done():
return
}
}
}()
return updateChan, errChan, nil
}
func (s *invoicesClient) AddHoldInvoice(ctx context.Context,
in *invoicesrpc.AddInvoiceData) (string, error) {
rpcCtx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
routeHints, err := marshallRouteHints(in.RouteHints)
if err != nil {
return "", fmt.Errorf("failed to marshal route hints: %v", err)
}
rpcIn := &invoicesrpc.AddHoldInvoiceRequest{
Memo: in.Memo,
Hash: in.Hash[:],
ValueMsat: int64(in.Value),
Expiry: in.Expiry,
CltvExpiry: in.CltvExpiry,
Private: in.Private,
RouteHints: routeHints,
DescriptionHash: in.DescriptionHash,
FallbackAddr: in.FallbackAddr,
}
rpcCtx = s.invoiceMac.WithMacaroonAuth(rpcCtx)
resp, err := s.client.AddHoldInvoice(rpcCtx, rpcIn)
if err != nil {
return "", err
}
return resp.PaymentRequest, nil
}
func fromRPCInvoiceState(state lnrpc.Invoice_InvoiceState) (
invpkg.ContractState, error) {
switch state {
case lnrpc.Invoice_OPEN:
return invpkg.ContractOpen, nil
case lnrpc.Invoice_ACCEPTED:
return invpkg.ContractAccepted, nil
case lnrpc.Invoice_SETTLED:
return invpkg.ContractSettled, nil
case lnrpc.Invoice_CANCELED:
return invpkg.ContractCanceled, nil
}
return 0, errors.New("unknown state")
}
// HtlcModifier is a bidirectional streaming RPC that allows a client to
// intercept and modify the HTLCs that attempt to settle the given invoice. The
// server will send HTLCs of invoices to the client and the client can modify
// some aspects of the HTLC in order to pass the invoice acceptance tests.
func (s *invoicesClient) HtlcModifier(ctx context.Context,
handler InvoiceHtlcModifyHandler) error {
// Create a child context that will be canceled when this function
// exits. We use this context to be able to cancel goroutines when we
// exit on errors, because the parent context won't be canceled in that
// case.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
stream, err := s.client.HtlcModifier(
s.invoiceMac.WithMacaroonAuth(ctx),
)
if err != nil {
return err
}
// Create an error channel that we'll send errors on if any of our
// goroutines fail. We buffer by 1 so that the goroutine doesn't depend
// on the stream being read, and select on context cancellation and
// quit channel so that we do not block in the case where we exit with
// multiple errors.
errChan := make(chan error, 1)
sendErr := func(err error) {
select {
case errChan <- err:
case <-ctx.Done():
case <-s.quit:
}
}
// Start a goroutine that consumes interception requests from lnd and
// sends them into our requests channel for handling. The requests
// channel is not buffered because we expect all requests to be handled
// until this function exits, at which point we expect our context to
// be canceled or quit channel to be closed.
requestChan := make(chan InvoiceHtlcModifyRequest)
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
// Do a quick check whether our client context has been
// canceled so that we can exit sooner if needed.
if ctx.Err() != nil {
return
}
req, err := stream.Recv()
if err != nil {
sendErr(err)
return
}
wireCustomRecords := req.ExitHtlcWireCustomRecords
interceptReq := InvoiceHtlcModifyRequest{
Invoice: req.Invoice,
CircuitKey: invpkg.CircuitKey{
ChanID: lnwire.NewShortChanIDFromInt(
req.ExitHtlcCircuitKey.ChanId,
),
HtlcID: req.ExitHtlcCircuitKey.HtlcId,
},
ExitHtlcAmt: lnwire.MilliSatoshi(
req.ExitHtlcAmt,
),
ExitHtlcExpiry: req.ExitHtlcExpiry,
CurrentHeight: req.CurrentHeight,
WireCustomRecords: wireCustomRecords,
}
// Try to send our interception request, failing on
// context cancel or router exit.
select {
case requestChan <- interceptReq:
case <-s.quit:
sendErr(ErrRouterShuttingDown)
return
case <-ctx.Done():
sendErr(ctx.Err())
return
}
}
}()
for {
select {
case request := <-requestChan:
// Handle requests in a goroutine so that the handler
// provided to this function can be blocking. If we
// get an error, send it into our error channel to
// shut down the interceptor.
s.wg.Add(1)
go func() {
defer s.wg.Done()
// Get a response from handler, this may block
// for a while.
resp, err := handler(ctx, request)
if err != nil {
sendErr(err)
return
}
key := resp.CircuitKey
amtPaid := uint64(resp.AmtPaid)
rpcResp := &invoicesrpc.HtlcModifyResponse{
CircuitKey: &invoicesrpc.CircuitKey{
ChanId: key.ChanID.ToUint64(),
HtlcId: key.HtlcID,
},
AmtPaid: &amtPaid,
CancelSet: resp.CancelSet,
}
if err := stream.Send(rpcResp); err != nil {
sendErr(err)
return
}
}()
// If one of our goroutines fails, exit with the error that
// occurred.
case err := <-errChan:
return err
case <-s.quit:
return ErrRouterShuttingDown
case <-ctx.Done():
return ctx.Err()
}
}
}