mirror of
https://github.com/lightninglabs/aperture.git
synced 2026-01-31 15:14:26 +01:00
lsat: remove pending token if payment failed
This commit is contained in:
@@ -3,6 +3,7 @@ package lsat
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync"
|
||||
@@ -58,6 +59,12 @@ var (
|
||||
authHeaderRegex = regexp.MustCompile(
|
||||
"LSAT macaroon=\"(.*?)\", invoice=\"(.*?)\"",
|
||||
)
|
||||
|
||||
// errPaymentFailedTerminally is signaled by the payment tracking method
|
||||
// to indicate a payment failed for good and will never change to a
|
||||
// success state.
|
||||
errPaymentFailedTerminally = errors.New("payment is in terminal " +
|
||||
"failure state")
|
||||
)
|
||||
|
||||
// ClientInterceptor is a gRPC client interceptor that can handle LSAT
|
||||
@@ -249,6 +256,29 @@ func (i *ClientInterceptor) handlePayment(iCtx *interceptContext) error {
|
||||
log.Infof("Payment of LSAT token is required, resuming/" +
|
||||
"tracking previous payment from pending LSAT token")
|
||||
err := i.trackPayment(iCtx.mainCtx, iCtx.token)
|
||||
|
||||
// If the payment failed for good, it will never come back to a
|
||||
// success state. We need to remove the pending token and try
|
||||
// again.
|
||||
if err == errPaymentFailedTerminally {
|
||||
iCtx.token = nil
|
||||
if err := i.store.RemovePendingToken(); err != nil {
|
||||
return fmt.Errorf("error removing pending "+
|
||||
"token, cannot retry payment: %v", err)
|
||||
}
|
||||
|
||||
// Let's try again by paying for the new token.
|
||||
log.Infof("Retrying payment of LSAT token invoice")
|
||||
var err error
|
||||
iCtx.token, err = i.payLsatToken(
|
||||
iCtx.mainCtx, iCtx.metadata,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -408,6 +438,13 @@ func (i *ClientInterceptor) trackPayment(ctx context.Context, token *Token) erro
|
||||
// time to complete.
|
||||
case lnrpc.Payment_IN_FLIGHT:
|
||||
|
||||
// The payment is in a terminal failed state, it will
|
||||
// never recover. There is no use keeping the pending
|
||||
// token around. So we signal the caller to remove it
|
||||
// and try again.
|
||||
case lnrpc.Payment_FAILED:
|
||||
return errPaymentFailedTerminally
|
||||
|
||||
// Any other state means either error or timeout.
|
||||
default:
|
||||
return fmt.Errorf("payment tracking failed "+
|
||||
|
||||
@@ -25,6 +25,7 @@ type interceptTestCase struct {
|
||||
interceptor *ClientInterceptor
|
||||
resetCb func()
|
||||
expectLndCall bool
|
||||
expectSecondLndCall bool
|
||||
sendPaymentCb func(*testing.T, test.PaymentChannelMessage)
|
||||
trackPaymentCb func(*testing.T, test.TrackPaymentMessage)
|
||||
expectToken bool
|
||||
@@ -54,6 +55,11 @@ func (s *mockStore) StoreToken(token *Token) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockStore) RemovePendingToken() error {
|
||||
s.token = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
lnd = test.NewMockLnd()
|
||||
store = &mockStore{}
|
||||
@@ -158,6 +164,46 @@ var (
|
||||
expectBackendCalls: 2,
|
||||
expectMacaroonCall1: false,
|
||||
expectMacaroonCall2: true,
|
||||
}, {
|
||||
name: "auth required, has pending but expired token",
|
||||
initialPreimage: &zeroPreimage,
|
||||
interceptor: interceptor,
|
||||
resetCb: func() {
|
||||
resetBackend(
|
||||
status.New(GRPCErrCode, GRPCErrMessage).Err(),
|
||||
makeAuthHeader(testMacBytes),
|
||||
)
|
||||
},
|
||||
expectLndCall: true,
|
||||
expectSecondLndCall: true,
|
||||
sendPaymentCb: func(t *testing.T,
|
||||
msg test.PaymentChannelMessage) {
|
||||
|
||||
require.Len(t, callMD, 0)
|
||||
|
||||
// The next call to the "backend" shouldn't return an
|
||||
// error.
|
||||
resetBackend(nil, "")
|
||||
msg.Done <- lndclient.PaymentResult{
|
||||
Preimage: paidPreimage,
|
||||
PaidAmt: 123,
|
||||
PaidFee: 345,
|
||||
}
|
||||
},
|
||||
trackPaymentCb: func(t *testing.T,
|
||||
msg test.TrackPaymentMessage) {
|
||||
|
||||
// The next call to the "backend" shouldn't return an
|
||||
// error.
|
||||
resetBackend(nil, "")
|
||||
msg.Updates <- lndclient.PaymentStatus{
|
||||
State: lnrpc.Payment_FAILED,
|
||||
}
|
||||
},
|
||||
expectToken: true,
|
||||
expectBackendCalls: 2,
|
||||
expectMacaroonCall1: false,
|
||||
expectMacaroonCall2: true,
|
||||
}, {
|
||||
name: "auth required, no token yet, cost limit",
|
||||
initialPreimage: nil,
|
||||
@@ -317,6 +363,18 @@ func testInterceptor(t *testing.T, tc interceptTestCase,
|
||||
t.Fatalf("[%s]: no payment request received", tc.name)
|
||||
}
|
||||
}
|
||||
if tc.expectSecondLndCall {
|
||||
select {
|
||||
case payment := <-lnd.SendPaymentChannel:
|
||||
tc.sendPaymentCb(t, payment)
|
||||
|
||||
case track := <-lnd.TrackPaymentChannel:
|
||||
tc.trackPaymentCb(t, track)
|
||||
|
||||
case <-time.After(testTimeout):
|
||||
t.Fatalf("[%s]: no payment request received", tc.name)
|
||||
}
|
||||
}
|
||||
backendWg.Wait()
|
||||
overallWg.Wait()
|
||||
|
||||
@@ -334,6 +392,8 @@ func testInterceptor(t *testing.T, tc interceptTestCase,
|
||||
if tc.expectToken {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, paidPreimage, storeToken.Preimage)
|
||||
} else {
|
||||
require.Equal(t, ErrNoToken, err)
|
||||
}
|
||||
if tc.expectMacaroonCall2 {
|
||||
require.Len(t, callMD, 1)
|
||||
|
||||
@@ -42,6 +42,10 @@ type Store interface {
|
||||
// StoreToken saves a token to the store. Old tokens should be kept for
|
||||
// accounting purposes but marked as invalid somehow.
|
||||
StoreToken(*Token) error
|
||||
|
||||
// RemovePendingToken removes a pending token from the store or returns
|
||||
// ErrNoToken if there is no pending token.
|
||||
RemovePendingToken() error
|
||||
}
|
||||
|
||||
// FileStore is an implementation of the Store interface that files to save the
|
||||
@@ -190,6 +194,16 @@ func (f *FileStore) StoreToken(newToken *Token) error {
|
||||
}
|
||||
}
|
||||
|
||||
// RemovePendingToken removes a pending token from the store or returns
|
||||
// ErrNoToken if there is no pending token.
|
||||
func (f *FileStore) RemovePendingToken() error {
|
||||
if !fileExists(f.fileNamePending) {
|
||||
return ErrNoToken
|
||||
}
|
||||
|
||||
return os.Remove(f.fileNamePending)
|
||||
}
|
||||
|
||||
// readTokenFile reads a single token from a file and returns it deserialized.
|
||||
func readTokenFile(tokenFile string) (*Token, error) {
|
||||
bytes, err := ioutil.ReadFile(tokenFile)
|
||||
|
||||
Reference in New Issue
Block a user