func extractPaymentDetails(token *Token, status lndclient.PaymentStatus) {
	token.Preimage = status.Preimage
	token.AmountPaid = status.Value
	token.RoutingFeePaid = status.Fee
}
extractPaymentDetails extracts the preimage and amounts paid for a payment
from the payment status and stores them in the token.
package lsat
import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"regexp"
	"sync"
	"time"
	"github.com/btcsuite/btcutil"
	"github.com/lightninglabs/lndclient"
	"github.com/lightningnetwork/lnd/lnrpc"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/zpay32"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)
const (
	GRPCErrCode = codes.Internal
	GRPCErrMessage = "payment required"
	AuthHeader = "WWW-Authenticate"
	DefaultMaxCostSats = 1000
	DefaultMaxRoutingFeeSats = 10
	PaymentTimeout = 60 * time.Second
	manualRetryHint = "consider removing pending token file if error " +
		"persists. use 'listauth' command to find out token file name"
)
var (
	authHeaderRegex = regexp.MustCompile(
		"LSAT macaroon=\"(.*?)\", invoice=\"(.*?)\"",
	)
	errPaymentFailedTerminally = errors.New("payment is in terminal " +
		"failure state")
)
type ClientInterceptor struct {
	lnd           *lndclient.LndServices
	store         Store
	callTimeout   time.Duration
	maxCost       btcutil.Amount
	maxFee        btcutil.Amount
	lock          sync.Mutex
	allowInsecure bool
}
func NewInterceptor(lnd *lndclient.LndServices, store Store,
	rpcCallTimeout time.Duration, maxCost,
	maxFee btcutil.Amount, allowInsecure bool) *ClientInterceptor {
	return &ClientInterceptor{
		lnd:           lnd,
		store:         store,
		callTimeout:   rpcCallTimeout,
		maxCost:       maxCost,
		maxFee:        maxFee,
		allowInsecure: allowInsecure,
	}
}
type interceptContext struct {
	mainCtx  context.Context
	opts     []grpc.CallOption
	metadata *metadata.MD
	token    *Token
}
func (i *ClientInterceptor) UnaryInterceptor(ctx context.Context, method string,
	req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
	opts ...grpc.CallOption) error {
	i.lock.Lock()
	defer i.lock.Unlock()
	iCtx, err := i.newInterceptContext(ctx, opts)
	if err != nil {
		return err
	}
	rpcCtx, cancel := context.WithTimeout(ctx, i.callTimeout)
	defer cancel()
	err = invoker(rpcCtx, method, req, reply, cc, iCtx.opts...)
	if !isPaymentRequired(err) {
		return err
	}
	err = i.handlePayment(iCtx)
	if err != nil {
		return err
	}
	rpcCtx2, cancel2 := context.WithTimeout(ctx, i.callTimeout)
	defer cancel2()
	return invoker(rpcCtx2, method, req, reply, cc, iCtx.opts...)
}
func (i *ClientInterceptor) StreamInterceptor(ctx context.Context,
	desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
	streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream,
	error) {
	i.lock.Lock()
	defer i.lock.Unlock()
	iCtx, err := i.newInterceptContext(ctx, opts)
	if err != nil {
		return nil, err
	}
	stream, err := streamer(ctx, desc, cc, method, iCtx.opts...)
	if !isPaymentRequired(err) {
		return stream, err
	}
	err = i.handlePayment(iCtx)
	if err != nil {
		return nil, err
	}
	return streamer(ctx, desc, cc, method, iCtx.opts...)
}
func (i *ClientInterceptor) newInterceptContext(ctx context.Context,
	opts []grpc.CallOption) (*interceptContext, error) {
	iCtx := &interceptContext{
		mainCtx:  ctx,
		opts:     opts,
		metadata: &metadata.MD{},
	}
	var err error
	iCtx.token, err = i.store.CurrentToken()
	switch {
	case err == ErrNoToken:
	case err != nil:
		log.Errorf("Failed to get token from store: %v", err)
		return nil, fmt.Errorf("getting token from store failed: %v",
			err)
	case !iCtx.token.isPending():
		if err = i.addLsatCredentials(iCtx); err != nil {
			log.Errorf("Adding macaroon to request failed: %v", err)
			return nil, fmt.Errorf("adding macaroon failed: %v",
				err)
		}
	}
	iCtx.opts = append(iCtx.opts, grpc.Trailer(iCtx.metadata))
	return iCtx, nil
}
func (i *ClientInterceptor) handlePayment(iCtx *interceptContext) error {
	switch {
	case iCtx.token != nil && iCtx.token.isPending():
		log.Infof("Payment of LSAT token is required, resuming/" +
			"tracking previous payment from pending LSAT token")
		err := i.trackPayment(iCtx.mainCtx, iCtx.token)
		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)
			}
			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
		}
	case iCtx.token == nil:
		log.Infof("Payment of LSAT token is required, paying invoice")
		var err error
		iCtx.token, err = i.payLsatToken(iCtx.mainCtx, iCtx.metadata)
		if err != nil {
			return err
		}
	default:
		log.Debugf("Found valid LSAT token to add to request")
	}
	if err := i.addLsatCredentials(iCtx); err != nil {
		log.Errorf("Adding macaroon to request failed: %v", err)
		return fmt.Errorf("adding macaroon failed: %v", err)
	}
	return nil
}
func (i *ClientInterceptor) addLsatCredentials(iCtx *interceptContext) error {
	if iCtx.token == nil {
		return fmt.Errorf("cannot add nil token to context")
	}
	macaroon, err := iCtx.token.PaidMacaroon()
	if err != nil {
		return err
	}
	iCtx.opts = append(iCtx.opts, grpc.PerRPCCredentials(
		NewMacaroonCredential(macaroon, i.allowInsecure),
	))
	return nil
}
func (i *ClientInterceptor) payLsatToken(ctx context.Context, md *metadata.MD) (
	*Token, error) {
	authHeader := md.Get(AuthHeader)
	if len(authHeader) == 0 {
		return nil, fmt.Errorf("auth header not found in response")
	}
	matches := authHeaderRegex.FindStringSubmatch(authHeader[0])
	if len(matches) != 3 {
		return nil, fmt.Errorf("invalid auth header "+
			"format: %s", authHeader[0])
	}
	macBase64, invoiceStr := matches[1], matches[2]
	macBytes, err := base64.StdEncoding.DecodeString(macBase64)
	if err != nil {
		return nil, fmt.Errorf("base64 decode of macaroon failed: "+
			"%v", err)
	}
	invoice, err := zpay32.Decode(invoiceStr, i.lnd.ChainParams)
	if err != nil {
		return nil, fmt.Errorf("unable to decode invoice: %v", err)
	}
	maxCostMsat := lnwire.NewMSatFromSatoshis(i.maxCost)
	if invoice.MilliSat != nil && *invoice.MilliSat > maxCostMsat {
		return nil, fmt.Errorf("cannot pay for LSAT automatically, "+
			"cost of %d msat exceeds configured max cost of %d "+
			"msat", *invoice.MilliSat, maxCostMsat)
	}
	token, err := tokenFromChallenge(macBytes, invoice.PaymentHash)
	if err != nil {
		return nil, fmt.Errorf("unable to create token: %v", err)
	}
	err = i.store.StoreToken(token)
	if err != nil {
		return nil, fmt.Errorf("unable to store pending token: %v", err)
	}
	payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
	defer cancel()
	respChan := i.lnd.Client.PayInvoice(
		payCtx, invoiceStr, i.maxFee, nil,
	)
	select {
	case result := <-respChan:
		if result.Err != nil {
			return nil, result.Err
		}
		token.Preimage = result.Preimage
		token.AmountPaid = lnwire.NewMSatFromSatoshis(result.PaidAmt)
		token.RoutingFeePaid = lnwire.NewMSatFromSatoshis(
			result.PaidFee,
		)
		return token, i.store.StoreToken(token)
	case <-payCtx.Done():
		return nil, fmt.Errorf("payment timed out. try again to track "+
			"payment. %s", manualRetryHint)
	case <-ctx.Done():
		return nil, fmt.Errorf("parent context canceled. try again to"+
			"track payment. %s", manualRetryHint)
	}
}
func (i *ClientInterceptor) trackPayment(ctx context.Context, token *Token) error {
	paymentStateCtx, cancel := context.WithCancel(ctx)
	defer cancel()
	payStatusChan, payErrChan, err := i.lnd.Router.TrackPayment(
		paymentStateCtx, token.PaymentHash,
	)
	if err != nil {
		log.Errorf("Could not call TrackPayment on lnd: %v", err)
		return fmt.Errorf("track payment call to lnd failed: %v", err)
	}
	payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
	defer cancel()
	for {
		select {
		case result := <-payStatusChan:
			switch result.State {
			case lnrpc.Payment_SUCCEEDED:
				extractPaymentDetails(token, result)
				return i.store.StoreToken(token)
			case lnrpc.Payment_IN_FLIGHT:
			case lnrpc.Payment_FAILED:
				return errPaymentFailedTerminally
			default:
				return fmt.Errorf("payment tracking failed "+
					"with state %s. %s",
					result.State.String(), manualRetryHint)
			}
		case err := <-payErrChan:
			return fmt.Errorf("payment tracking failed: %v. %s",
				err, manualRetryHint)
		case <-payCtx.Done():
			return fmt.Errorf("payment tracking timed out. %s",
				manualRetryHint)
		}
	}
}
func isPaymentRequired(err error) bool {
	statusErr, ok := status.FromError(err)
	return ok &&
		statusErr.Message() == GRPCErrMessage &&
		statusErr.Code() == GRPCErrCode
}