@@ -6,8 +6,10 @@ import (
66 "fmt"
77 "regexp"
88 "sync"
9+ "time"
910
1011 "github.com/lightninglabs/loop/lndclient"
12+ "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1113 "github.com/lightningnetwork/lnd/lnwire"
1214 "github.com/lightningnetwork/lnd/macaroons"
1315 "github.com/lightningnetwork/lnd/zpay32"
@@ -35,6 +37,15 @@ const (
3537 // going to pay to acquire an LSAT token.
3638 // TODO(guggero): make this configurable
3739 MaxRoutingFeeSats = 10
40+
41+ // PaymentTimeout is the maximum time we allow a payment to take before
42+ // we stop waiting for it.
43+ PaymentTimeout = 60 * time .Second
44+
45+ // manualRetryHint is the error text we return to tell the user how a
46+ // token payment can be retried if the payment fails.
47+ manualRetryHint = "consider removing pending token file if error " +
48+ "persists. use 'listauth' command to find out token file name"
3849)
3950
4051var (
@@ -91,36 +102,49 @@ func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string,
91102 return nil
92103 }
93104
94- // If we already have a token, let's append it.
95- if i .store .HasToken () {
96- lsat , err := i .store .Token ()
97- if err != nil {
98- return err
99- }
100- if err = addLsatCredentials (lsat ); err != nil {
101- return err
105+ // Let's see if the store already contains a token and what state it
106+ // might be in. If a previous call was aborted, we might have a pending
107+ // token that needs to be handled separately.
108+ token , err := i .store .CurrentToken ()
109+ switch {
110+ // If there is no token yet, nothing to do at this point.
111+ case err == ErrNoToken :
112+
113+ // Some other error happened that we have to surface.
114+ case err != nil :
115+ log .Errorf ("Failed to get token from store: %v" , err )
116+ return fmt .Errorf ("getting token from store failed: %v" , err )
117+
118+ // Only if we have a paid token append it. We don't resume a pending
119+ // payment just yet, since we don't even know if a token is required for
120+ // this call. We also never send a pending payment to the server since
121+ // we know it's not valid.
122+ case ! token .isPending ():
123+ if err = addLsatCredentials (token ); err != nil {
124+ log .Errorf ("Adding macaroon to request failed: %v" , err )
125+ return fmt .Errorf ("adding macaroon failed: %v" , err )
102126 }
103127 }
104128
105- // We need a way to extract the response headers sent by the
106- // server. This can only be done through the experimental
107- // grpc.Trailer call option.
108- // We execute the request and inspect the error. If it's the
109- // LSAT specific payment required error, we might execute the
110- // same method again later with the paid LSAT token.
129+ // We need a way to extract the response headers sent by the server.
130+ // This can only be done through the experimental grpc.Trailer call
131+ // option. We execute the request and inspect the error. If it's the
132+ // LSAT specific payment required error, we might execute the same
133+ // method again later with the paid LSAT token.
111134 trailerMetadata := & metadata.MD {}
112135 opts = append (opts , grpc .Trailer (trailerMetadata ))
113- err : = invoker (ctx , method , req , reply , cc , opts ... )
136+ err = invoker (ctx , method , req , reply , cc , opts ... )
114137
115138 // Only handle the LSAT error message that comes in the form of
116139 // a gRPC status error.
117140 if isPaymentRequired (err ) {
118- lsat , err := i .payLsatToken (ctx , trailerMetadata )
141+ paidToken , err := i .handlePayment (ctx , token , trailerMetadata )
119142 if err != nil {
120143 return err
121144 }
122- if err = addLsatCredentials (lsat ); err != nil {
123- return err
145+ if err = addLsatCredentials (paidToken ); err != nil {
146+ log .Errorf ("Adding macaroon to request failed: %v" , err )
147+ return fmt .Errorf ("adding macaroon failed: %v" , err )
124148 }
125149
126150 // Execute the same request again, now with the LSAT
@@ -130,6 +154,35 @@ func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string,
130154 return err
131155}
132156
157+ // handlePayment tries to obtain a valid token by either tracking the payment
158+ // status of a pending token or paying for a new one.
159+ func (i * Interceptor ) handlePayment (ctx context.Context , token * Token ,
160+ md * metadata.MD ) (* Token , error ) {
161+
162+ switch {
163+ // Resume/track a pending payment if it was interrupted for some reason.
164+ case token != nil && token .isPending ():
165+ log .Infof ("Payment of LSAT token is required, resuming/" +
166+ "tracking previous payment from pending LSAT token" )
167+ err := i .trackPayment (ctx , token )
168+ if err != nil {
169+ return nil , err
170+ }
171+ return token , nil
172+
173+ // We don't have a token yet, try to get a new one.
174+ case token == nil :
175+ // We don't have a token yet, get a new one.
176+ log .Infof ("Payment of LSAT token is required, paying invoice" )
177+ return i .payLsatToken (ctx , md )
178+
179+ // We have a token and it's valid, nothing more to do here.
180+ default :
181+ log .Debugf ("Found valid LSAT token to add to request" )
182+ return token , nil
183+ }
184+ }
185+
133186// payLsatToken reads the payment challenge from the response metadata and tries
134187// to pay the invoice encoded in them, returning a paid LSAT token if
135188// successful.
@@ -161,31 +214,100 @@ func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) (
161214 return nil , fmt .Errorf ("unable to decode invoice: %v" , err )
162215 }
163216
217+ // Create and store the pending token so we can resume the payment in
218+ // case the payment is interrupted somehow.
219+ token , err := tokenFromChallenge (macBytes , invoice .PaymentHash )
220+ if err != nil {
221+ return nil , fmt .Errorf ("unable to create token: %v" , err )
222+ }
223+ err = i .store .StoreToken (token )
224+ if err != nil {
225+ return nil , fmt .Errorf ("unable to store pending token: %v" , err )
226+ }
227+
164228 // Pay invoice now and wait for the result to arrive or the main context
165229 // being canceled.
166- // TODO(guggero): Store payment information so we can track the payment
167- // later in case the client shuts down while the payment is in flight.
230+ payCtx , cancel := context . WithTimeout ( ctx , PaymentTimeout )
231+ defer cancel ()
168232 respChan := i .lnd .Client .PayInvoice (
169- ctx , invoiceStr , MaxRoutingFeeSats , nil ,
233+ payCtx , invoiceStr , MaxRoutingFeeSats , nil ,
170234 )
171235 select {
172236 case result := <- respChan :
173237 if result .Err != nil {
174238 return nil , result .Err
175239 }
176- token , err := NewToken (
177- macBytes , invoice . PaymentHash , result .Preimage ,
178- lnwire .NewMSatFromSatoshis (result . PaidAmt ),
179- lnwire . NewMSatFromSatoshis ( result .PaidFee ) ,
240+ token . Preimage = result . Preimage
241+ token . AmountPaid = lnwire . NewMSatFromSatoshis ( result .PaidAmt )
242+ token . RoutingFeePaid = lnwire .NewMSatFromSatoshis (
243+ result .PaidFee ,
180244 )
181- if err != nil {
182- return nil , fmt .Errorf ("unable to create token: %v" ,
183- err )
184- }
185245 return token , i .store .StoreToken (token )
186246
247+ case <- payCtx .Done ():
248+ return nil , fmt .Errorf ("payment timed out. try again to track " +
249+ "payment. %s" , manualRetryHint )
250+
187251 case <- ctx .Done ():
188- return nil , fmt .Errorf ("context canceled" )
252+ return nil , fmt .Errorf ("parent context canceled. try again to" +
253+ "track payment. %s" , manualRetryHint )
254+ }
255+ }
256+
257+ // trackPayment tries to resume a pending payment by tracking its state and
258+ // waiting for a conclusive result.
259+ func (i * Interceptor ) trackPayment (ctx context.Context , token * Token ) error {
260+ // Lookup state of the payment.
261+ paymentStateCtx , cancel := context .WithCancel (ctx )
262+ defer cancel ()
263+ payStatusChan , payErrChan , err := i .lnd .Router .TrackPayment (
264+ paymentStateCtx , token .PaymentHash ,
265+ )
266+ if err != nil {
267+ log .Errorf ("Could not call TrackPayment on lnd: %v" , err )
268+ return fmt .Errorf ("track payment call to lnd failed: %v" , err )
269+ }
270+
271+ // We can't wait forever, so we give the payment tracking the same
272+ // timeout as the original payment.
273+ payCtx , cancel := context .WithTimeout (ctx , PaymentTimeout )
274+ defer cancel ()
275+
276+ // We'll consume status updates until we reach a conclusive state or
277+ // reach the timeout.
278+ for {
279+ select {
280+ // If we receive a state without an error, the payment has been
281+ // initiated. Loop until the payment
282+ case result := <- payStatusChan :
283+ switch result .State {
284+ // If the payment was successful, we have all the
285+ // information we need and we can return the fully paid
286+ // token.
287+ case routerrpc .PaymentState_SUCCEEDED :
288+ extractPaymentDetails (token , result )
289+ return i .store .StoreToken (token )
290+
291+ // The payment is still in transit, we'll give it more
292+ // time to complete.
293+ case routerrpc .PaymentState_IN_FLIGHT :
294+
295+ // Any other state means either error or timeout.
296+ default :
297+ return fmt .Errorf ("payment tracking failed " +
298+ "with state %s. %s" ,
299+ result .State .String (), manualRetryHint )
300+ }
301+
302+ // Abort the payment execution for any error.
303+ case err := <- payErrChan :
304+ return fmt .Errorf ("payment tracking failed: %v. %s" ,
305+ err , manualRetryHint )
306+
307+ case <- payCtx .Done ():
308+ return fmt .Errorf ("payment tracking timed out. %s" ,
309+ manualRetryHint )
310+ }
189311 }
190312}
191313
@@ -198,3 +320,13 @@ func isPaymentRequired(err error) bool {
198320 statusErr .Message () == GRPCErrMessage &&
199321 statusErr .Code () == GRPCErrCode
200322}
323+
324+ // extractPaymentDetails extracts the preimage and amounts paid for a payment
325+ // from the payment status and stores them in the token.
326+ func extractPaymentDetails (token * Token , status lndclient.PaymentStatus ) {
327+ token .Preimage = status .Preimage
328+ total := status .Route .TotalAmount
329+ fees := status .Route .TotalFees ()
330+ token .AmountPaid = total - fees
331+ token .RoutingFeePaid = fees
332+ }
0 commit comments