Merge pull request #101 from guggero/lsat-integration

LSAT aware client interceptor
pull/128/head
Olaoluwa Osuntokun 4 years ago committed by GitHub
commit 53dc21f99a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/lsat"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/sweep"
"github.com/lightningnetwork/lnd/lntypes"
@ -48,8 +49,15 @@ var (
ErrSweepConfTargetTooFar = errors.New("sweep confirmation target is " +
"beyond swap expiration height")
// serverRPCTimeout is the maximum time a gRPC request to the server
// should be allowed to take.
serverRPCTimeout = 30 * time.Second
// globalCallTimeout is the maximum time any call of the client to the
// server is allowed to take, including the time it may take to get
// and pay for an LSAT token.
globalCallTimeout = serverRPCTimeout + lsat.PaymentTimeout
republishDelay = 10 * time.Second
)
@ -71,14 +79,21 @@ type Client struct {
// NewClient returns a new instance to initiate swaps with.
func NewClient(dbDir string, serverAddress string, insecure bool,
lnd *lndclient.LndServices) (*Client, func(), error) {
tlsPathServer string, lnd *lndclient.LndServices) (*Client, func(),
error) {
store, err := loopdb.NewBoltSwapStore(dbDir, lnd.ChainParams)
if err != nil {
return nil, nil, err
}
lsatStore, err := lsat.NewFileStore(dbDir)
if err != nil {
return nil, nil, err
}
swapServerClient, err := newSwapServerClient(serverAddress, insecure)
swapServerClient, err := newSwapServerClient(
serverAddress, insecure, tlsPathServer, lsatStore, lnd,
)
if err != nil {
return nil, nil, err
}
@ -87,6 +102,7 @@ func NewClient(dbDir string, serverAddress string, insecure bool,
LndServices: lnd,
Server: swapServerClient,
Store: store,
LsatStore: lsatStore,
CreateExpiryTimer: func(d time.Duration) <-chan time.Time {
return time.NewTimer(d).C
},

@ -0,0 +1,82 @@
package main
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"time"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/lsat"
"github.com/urfave/cli"
"gopkg.in/macaroon.v2"
)
type printableToken struct {
ID string `json:"id"`
ValidUntil string `json:"valid_until"`
BaseMacaroon string `json:"base_macaroon"`
PaymentHash string `json:"payment_hash"`
PaymentPreimage string `json:"payment_preimage"`
AmountPaid int64 `json:"amount_paid_msat"`
RoutingFeePaid int64 `json:"routing_fee_paid_msat"`
TimeCreated string `json:"time_created"`
Expired bool `json:"expired"`
FileName string `json:"file_name"`
}
var listAuthCommand = cli.Command{
Name: "listauth",
Usage: "list all LSAT tokens",
Description: "Shows a list of all LSAT tokens that loopd has paid for",
Action: listAuth,
}
func listAuth(ctx *cli.Context) error {
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
resp, err := client.GetLsatTokens(
context.Background(), &looprpc.TokensRequest{},
)
if err != nil {
return err
}
tokens := make([]*printableToken, len(resp.Tokens))
for i, t := range resp.Tokens {
mac := &macaroon.Macaroon{}
err := mac.UnmarshalBinary(t.BaseMacaroon)
if err != nil {
return fmt.Errorf("unable to unmarshal macaroon: %v",
err)
}
id, err := lsat.DecodeIdentifier(bytes.NewReader(mac.Id()))
if err != nil {
return fmt.Errorf("unable to decode macaroon ID: %v",
err)
}
tokens[i] = &printableToken{
ID: hex.EncodeToString(id.TokenID[:]),
ValidUntil: "",
BaseMacaroon: hex.EncodeToString(t.BaseMacaroon),
PaymentHash: hex.EncodeToString(t.PaymentHash),
PaymentPreimage: hex.EncodeToString(t.PaymentPreimage),
AmountPaid: t.AmountPaidMsat,
RoutingFeePaid: t.RoutingFeePaidMsat,
TimeCreated: time.Unix(t.TimeCreated, 0).Format(
time.RFC3339,
),
Expired: t.Expired,
FileName: t.StorageName,
}
}
printJSON(tokens)
return nil
}

@ -1,6 +1,8 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
@ -30,6 +32,21 @@ var (
defaultSwapWaitTime = 30 * time.Minute
)
func printJSON(resp interface{}) {
b, err := json.Marshal(resp)
if err != nil {
fatal(err)
}
var out bytes.Buffer
err = json.Indent(&out, b, "", "\t")
if err != nil {
fatal(err)
}
out.WriteString("\n")
_, _ = out.WriteTo(os.Stdout)
}
func printRespJSON(resp proto.Message) {
jsonMarshaler := &jsonpb.Marshaler{
EmitDefaults: true,
@ -65,7 +82,7 @@ func main() {
}
app.Commands = []cli.Command{
loopOutCommand, loopInCommand, termsCommand,
monitorCommand, quoteCommand,
monitorCommand, quoteCommand, listAuthCommand,
}
err := app.Run(os.Args)

@ -27,12 +27,13 @@ type lndConfig struct {
type viewParameters struct{}
type config struct {
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
Insecure bool `long:"insecure" description:"disable tls"`
Network string `long:"network" description:"network to run on" choice:"regtest" choice:"testnet" choice:"mainnet" choice:"simnet"`
SwapServer string `long:"swapserver" description:"swap server address host:port"`
RPCListen string `long:"rpclisten" description:"Address to listen on for gRPC clients"`
RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"`
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
Insecure bool `long:"insecure" description:"disable tls"`
Network string `long:"network" description:"network to run on" choice:"regtest" choice:"testnet" choice:"mainnet" choice:"simnet"`
SwapServer string `long:"swapserver" description:"swap server address host:port"`
TLSPathSwapSrv string `long:"tlspathswapserver" description:"Path to swap server tls certificate. Only needed if the swap server uses a self-signed certificate."`
RPCListen string `long:"rpclisten" description:"Address to listen on for gRPC clients"`
RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"`
LogDir string `long:"logdir" description:"Directory to log output."`
MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"`

@ -45,7 +45,7 @@ func daemon(config *config) error {
// Create an instance of the loop client library.
swapClient, cleanup, err := getClient(
config.Network, config.SwapServer, config.Insecure,
&lnd.LndServices,
config.TLSPathSwapSrv, &lnd.LndServices,
)
if err != nil {
return err

@ -5,6 +5,7 @@ import (
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/lsat"
"github.com/lightningnetwork/lnd/build"
)
@ -19,6 +20,7 @@ func init() {
addSubLogger("LOOP", loop.UseLogger)
addSubLogger("LNDC", lndclient.UseLogger)
addSubLogger("STORE", loopdb.UseLogger)
addSubLogger(lsat.Subsystem, lsat.UseLogger)
}
// addSubLogger is a helper method to conveniently create and register the

@ -346,6 +346,40 @@ func (s *swapClientServer) LoopIn(ctx context.Context,
}, nil
}
// GetLsatTokens returns all tokens that are contained in the LSAT token store.
func (s *swapClientServer) GetLsatTokens(ctx context.Context,
_ *looprpc.TokensRequest) (*looprpc.TokensResponse, error) {
log.Infof("Get LSAT tokens request received")
tokens, err := s.impl.LsatStore.AllTokens()
if err != nil {
return nil, err
}
rpcTokens := make([]*looprpc.LsatToken, len(tokens))
idx := 0
for key, token := range tokens {
macBytes, err := token.BaseMacaroon().MarshalBinary()
if err != nil {
return nil, err
}
rpcTokens[idx] = &looprpc.LsatToken{
BaseMacaroon: macBytes,
PaymentHash: token.PaymentHash[:],
PaymentPreimage: token.Preimage[:],
AmountPaidMsat: int64(token.AmountPaid),
RoutingFeePaidMsat: int64(token.RoutingFeePaid),
TimeCreated: token.TimeCreated.Unix(),
Expired: !token.IsValid(),
StorageName: key,
}
idx++
}
return &looprpc.TokensResponse{Tokens: rpcTokens}, nil
}
// validateConfTarget ensures the given confirmation target is valid. If one
// isn't specified (0 value), then the default target is used.
func validateConfTarget(target, defaultTarget int32) (int32, error) {

@ -16,7 +16,7 @@ func getLnd(network string, cfg *lndConfig) (*lndclient.GrpcLndServices, error)
}
// getClient returns an instance of the swap client.
func getClient(network, swapServer string, insecure bool,
func getClient(network, swapServer string, insecure bool, tlsPathServer string,
lnd *lndclient.LndServices) (*loop.Client, func(), error) {
storeDir, err := getStoreDir(network)
@ -25,7 +25,7 @@ func getClient(network, swapServer string, insecure bool,
}
swapClient, cleanUp, err := loop.NewClient(
storeDir, swapServer, insecure, lnd,
storeDir, swapServer, insecure, tlsPathServer, lnd,
)
if err != nil {
return nil, nil, err

@ -24,7 +24,8 @@ func view(config *config) error {
defer lnd.Close()
swapClient, cleanup, err := getClient(
config.Network, config.SwapServer, config.Insecure, &lnd.LndServices,
config.Network, config.SwapServer, config.Insecure,
config.TLSPathSwapSrv, &lnd.LndServices,
)
if err != nil {
return err

@ -5,6 +5,7 @@ import (
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/lsat"
)
// clientConfig contains config items for the swap client.
@ -12,5 +13,6 @@ type clientConfig struct {
LndServices *lndclient.LndServices
Server swapServerClient
Store loopdb.SwapStore
LsatStore lsat.Store
CreateExpiryTimer func(expiry time.Duration) <-chan time.Time
}

@ -801,6 +801,192 @@ func (m *QuoteResponse) GetCltvDelta() int32 {
return 0
}
type TokensRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *TokensRequest) Reset() { *m = TokensRequest{} }
func (m *TokensRequest) String() string { return proto.CompactTextString(m) }
func (*TokensRequest) ProtoMessage() {}
func (*TokensRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_014de31d7ac8c57c, []int{9}
}
func (m *TokensRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TokensRequest.Unmarshal(m, b)
}
func (m *TokensRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_TokensRequest.Marshal(b, m, deterministic)
}
func (m *TokensRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_TokensRequest.Merge(m, src)
}
func (m *TokensRequest) XXX_Size() int {
return xxx_messageInfo_TokensRequest.Size(m)
}
func (m *TokensRequest) XXX_DiscardUnknown() {
xxx_messageInfo_TokensRequest.DiscardUnknown(m)
}
var xxx_messageInfo_TokensRequest proto.InternalMessageInfo
type TokensResponse struct {
//*
//List of all tokens the daemon knows of, including old/expired tokens.
Tokens []*LsatToken `protobuf:"bytes,1,rep,name=tokens,proto3" json:"tokens,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *TokensResponse) Reset() { *m = TokensResponse{} }
func (m *TokensResponse) String() string { return proto.CompactTextString(m) }
func (*TokensResponse) ProtoMessage() {}
func (*TokensResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_014de31d7ac8c57c, []int{10}
}
func (m *TokensResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TokensResponse.Unmarshal(m, b)
}
func (m *TokensResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_TokensResponse.Marshal(b, m, deterministic)
}
func (m *TokensResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_TokensResponse.Merge(m, src)
}
func (m *TokensResponse) XXX_Size() int {
return xxx_messageInfo_TokensResponse.Size(m)
}
func (m *TokensResponse) XXX_DiscardUnknown() {
xxx_messageInfo_TokensResponse.DiscardUnknown(m)
}
var xxx_messageInfo_TokensResponse proto.InternalMessageInfo
func (m *TokensResponse) GetTokens() []*LsatToken {
if m != nil {
return m.Tokens
}
return nil
}
type LsatToken struct {
//*
//The base macaroon that was baked by the auth server.
BaseMacaroon []byte `protobuf:"bytes,1,opt,name=base_macaroon,json=baseMacaroon,proto3" json:"base_macaroon,omitempty"`
//*
//The payment hash of the payment that was paid to obtain the token.
PaymentHash []byte `protobuf:"bytes,2,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"`
//*
//The preimage of the payment hash, knowledge of this is proof that the
//payment has been paid. If the preimage is set to all zeros, this means the
//payment is still pending and the token is not yet fully valid.
PaymentPreimage []byte `protobuf:"bytes,3,opt,name=payment_preimage,json=paymentPreimage,proto3" json:"payment_preimage,omitempty"`
//*
//The amount of millisatoshis that was paid to get the token.
AmountPaidMsat int64 `protobuf:"varint,4,opt,name=amount_paid_msat,json=amountPaidMsat,proto3" json:"amount_paid_msat,omitempty"`
//*
//The amount of millisatoshis paid in routing fee to pay for the token.
RoutingFeePaidMsat int64 `protobuf:"varint,5,opt,name=routing_fee_paid_msat,json=routingFeePaidMsat,proto3" json:"routing_fee_paid_msat,omitempty"`
//*
//The creation time of the token as UNIX timestamp in seconds.
TimeCreated int64 `protobuf:"varint,6,opt,name=time_created,json=timeCreated,proto3" json:"time_created,omitempty"`
//*
//Indicates whether the token is expired or still valid.
Expired bool `protobuf:"varint,7,opt,name=expired,proto3" json:"expired,omitempty"`
//*
//Identifying attribute of this token in the store. Currently represents the
//file name of the token where it's stored on the file system.
StorageName string `protobuf:"bytes,8,opt,name=storage_name,json=storageName,proto3" json:"storage_name,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *LsatToken) Reset() { *m = LsatToken{} }
func (m *LsatToken) String() string { return proto.CompactTextString(m) }
func (*LsatToken) ProtoMessage() {}
func (*LsatToken) Descriptor() ([]byte, []int) {
return fileDescriptor_014de31d7ac8c57c, []int{11}
}
func (m *LsatToken) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_LsatToken.Unmarshal(m, b)
}
func (m *LsatToken) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_LsatToken.Marshal(b, m, deterministic)
}
func (m *LsatToken) XXX_Merge(src proto.Message) {
xxx_messageInfo_LsatToken.Merge(m, src)
}
func (m *LsatToken) XXX_Size() int {
return xxx_messageInfo_LsatToken.Size(m)
}
func (m *LsatToken) XXX_DiscardUnknown() {
xxx_messageInfo_LsatToken.DiscardUnknown(m)
}
var xxx_messageInfo_LsatToken proto.InternalMessageInfo
func (m *LsatToken) GetBaseMacaroon() []byte {
if m != nil {
return m.BaseMacaroon
}
return nil
}
func (m *LsatToken) GetPaymentHash() []byte {
if m != nil {
return m.PaymentHash
}
return nil
}
func (m *LsatToken) GetPaymentPreimage() []byte {
if m != nil {
return m.PaymentPreimage
}
return nil
}
func (m *LsatToken) GetAmountPaidMsat() int64 {
if m != nil {
return m.AmountPaidMsat
}
return 0
}
func (m *LsatToken) GetRoutingFeePaidMsat() int64 {
if m != nil {
return m.RoutingFeePaidMsat
}
return 0
}
func (m *LsatToken) GetTimeCreated() int64 {
if m != nil {
return m.TimeCreated
}
return 0
}
func (m *LsatToken) GetExpired() bool {
if m != nil {
return m.Expired
}
return false
}
func (m *LsatToken) GetStorageName() string {
if m != nil {
return m.StorageName
}
return ""
}
func init() {
proto.RegisterEnum("looprpc.SwapType", SwapType_name, SwapType_value)
proto.RegisterEnum("looprpc.SwapState", SwapState_name, SwapState_value)
@ -813,81 +999,97 @@ func init() {
proto.RegisterType((*TermsResponse)(nil), "looprpc.TermsResponse")
proto.RegisterType((*QuoteRequest)(nil), "looprpc.QuoteRequest")
proto.RegisterType((*QuoteResponse)(nil), "looprpc.QuoteResponse")
proto.RegisterType((*TokensRequest)(nil), "looprpc.TokensRequest")
proto.RegisterType((*TokensResponse)(nil), "looprpc.TokensResponse")
proto.RegisterType((*LsatToken)(nil), "looprpc.LsatToken")
}
func init() { proto.RegisterFile("client.proto", fileDescriptor_014de31d7ac8c57c) }
var fileDescriptor_014de31d7ac8c57c = []byte{
// 1096 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0x4f, 0x73, 0xda, 0xc6,
0x1b, 0x8e, 0x40, 0x18, 0xf1, 0x5a, 0xc8, 0x62, 0x9d, 0xd8, 0x98, 0xdf, 0x2f, 0x13, 0xaa, 0x36,
0x29, 0xe3, 0x43, 0x68, 0x93, 0x53, 0x73, 0xa3, 0x40, 0x12, 0x3c, 0xb6, 0xa1, 0x02, 0x67, 0xa6,
0xbd, 0xa8, 0x1b, 0x58, 0x6c, 0xcd, 0x48, 0x2b, 0x45, 0x5a, 0xfc, 0x67, 0x3a, 0xb9, 0xf4, 0x2b,
0xf4, 0xd6, 0x6f, 0xd1, 0x99, 0x7e, 0x93, 0xde, 0x7b, 0xea, 0xf4, 0x73, 0x74, 0xf6, 0x5d, 0x81,
0x85, 0x89, 0x2f, 0xb9, 0x99, 0x67, 0x9f, 0x7d, 0xde, 0x3f, 0xfb, 0xbc, 0xaf, 0x0c, 0xe6, 0x34,
0xf0, 0x19, 0x17, 0xcf, 0xe3, 0x24, 0x12, 0x11, 0x29, 0x07, 0x51, 0x14, 0x27, 0xf1, 0xb4, 0xf1,
0xff, 0xf3, 0x28, 0x3a, 0x0f, 0x58, 0x9b, 0xc6, 0x7e, 0x9b, 0x72, 0x1e, 0x09, 0x2a, 0xfc, 0x88,
0xa7, 0x8a, 0xe6, 0xfc, 0x5e, 0x04, 0xeb, 0x38, 0x8a, 0xe2, 0xe1, 0x42, 0xb8, 0xec, 0xc3, 0x82,
0xa5, 0x82, 0xd8, 0x50, 0xa4, 0xa1, 0xa8, 0x6b, 0x4d, 0xad, 0x55, 0x74, 0xe5, 0x9f, 0x84, 0x80,
0x3e, 0x63, 0xa9, 0xa8, 0x17, 0x9a, 0x5a, 0xab, 0xe2, 0xe2, 0xdf, 0xa4, 0x0d, 0x0f, 0x43, 0x7a,
0xed, 0xa5, 0x57, 0x34, 0xf6, 0x92, 0x68, 0x21, 0x7c, 0x7e, 0xee, 0xcd, 0x19, 0xab, 0x17, 0xf1,
0x5a, 0x2d, 0xa4, 0xd7, 0xe3, 0x2b, 0x1a, 0xbb, 0xea, 0xe4, 0x35, 0x63, 0xe4, 0x25, 0xec, 0xc9,
0x0b, 0x71, 0xc2, 0x62, 0x7a, 0xb3, 0x76, 0x45, 0xc7, 0x2b, 0xbb, 0x21, 0xbd, 0x1e, 0xe1, 0x61,
0xee, 0x52, 0x13, 0xcc, 0x55, 0x14, 0x49, 0x2d, 0x21, 0x15, 0x32, 0x75, 0xc9, 0xf8, 0x0a, 0xac,
0x9c, 0xac, 0x4c, 0x7c, 0x0b, 0x39, 0xe6, 0x4a, 0xae, 0x13, 0x0a, 0xe2, 0x40, 0x55, 0xb2, 0x42,
0x9f, 0xb3, 0x04, 0x85, 0xca, 0x48, 0xda, 0x0e, 0xe9, 0xf5, 0x89, 0xc4, 0xa4, 0x52, 0x0b, 0x6c,
0xd9, 0x33, 0x2f, 0x5a, 0x08, 0x6f, 0x7a, 0x41, 0x39, 0x67, 0x41, 0xdd, 0x68, 0x6a, 0x2d, 0xdd,
0xb5, 0x02, 0xd5, 0xa1, 0xae, 0x42, 0xc9, 0x21, 0xd4, 0xd2, 0x2b, 0xc6, 0x62, 0x6f, 0x1a, 0xf1,
0xb9, 0x27, 0x68, 0x72, 0xce, 0x44, 0xbd, 0xd2, 0xd4, 0x5a, 0x25, 0x77, 0x07, 0x0f, 0xba, 0x11,
0x9f, 0x4f, 0x10, 0x26, 0xaf, 0xe0, 0x00, 0xb3, 0x8f, 0x17, 0xef, 0x03, 0x7f, 0x8a, 0xbd, 0xf7,
0x66, 0x8c, 0xce, 0x02, 0x9f, 0xb3, 0x3a, 0xa0, 0xfc, 0xbe, 0x24, 0x8c, 0x6e, 0xcf, 0x7b, 0xd9,
0xb1, 0xf3, 0xa7, 0x06, 0x55, 0xf9, 0x38, 0x03, 0x7e, 0xff, 0xdb, 0xdc, 0xed, 0x50, 0x61, 0xa3,
0x43, 0x1b, 0xb5, 0x17, 0x37, 0x6b, 0x7f, 0x06, 0x3b, 0x58, 0xbb, 0xcf, 0x57, 0xa5, 0xeb, 0x98,
0x5b, 0x35, 0xc0, 0xf8, 0xcb, 0xca, 0xbf, 0x84, 0x2a, 0xbb, 0x16, 0x2c, 0xe1, 0x34, 0xf0, 0x2e,
0x44, 0x30, 0xc5, 0x07, 0x31, 0x5c, 0x73, 0x09, 0xbe, 0x15, 0xc1, 0xd4, 0xe9, 0x80, 0x89, 0x6f,
0xcf, 0xd2, 0x38, 0xe2, 0x29, 0x23, 0x16, 0x14, 0xfc, 0x19, 0xe6, 0x5c, 0x71, 0x0b, 0xfe, 0x8c,
0x7c, 0x01, 0xa6, 0xbc, 0xeb, 0xd1, 0xd9, 0x2c, 0x61, 0x69, 0x9a, 0xd9, 0x6a, 0x5b, 0x62, 0x1d,
0x05, 0x39, 0x36, 0x58, 0x27, 0x11, 0xf7, 0x45, 0x94, 0x64, 0x95, 0x3b, 0x7f, 0x17, 0x00, 0xa4,
0xea, 0x58, 0x50, 0xb1, 0x48, 0x3f, 0xd1, 0x08, 0x15, 0xa5, 0xb0, 0x8a, 0xf2, 0x14, 0x74, 0x71,
0x13, 0xab, 0x6a, 0xad, 0x17, 0xb5, 0xe7, 0xd9, 0x3c, 0x3c, 0x97, 0x22, 0x93, 0x9b, 0x98, 0xb9,
0x78, 0x4c, 0x5a, 0x50, 0x4a, 0x05, 0x15, 0xca, 0x85, 0xd6, 0x0b, 0xb2, 0xc6, 0x93, 0xc1, 0x98,
0xab, 0x08, 0xe4, 0x6b, 0xd8, 0xf1, 0xb9, 0x2f, 0x7c, 0xf5, 0x86, 0xc2, 0x0f, 0x97, 0x76, 0xb4,
0x6e, 0xe1, 0x89, 0x1f, 0x2a, 0x23, 0xd1, 0x54, 0x78, 0x8b, 0x78, 0x46, 0x05, 0x53, 0x4c, 0x65,
0x4a, 0x4b, 0xe2, 0x67, 0x08, 0x23, 0xf3, 0x6e, 0x27, 0xca, 0x1b, 0x9d, 0x20, 0x4f, 0x60, 0x7b,
0x1a, 0xa5, 0xc2, 0x4b, 0x59, 0x72, 0xc9, 0x12, 0x34, 0x64, 0xd1, 0x05, 0x09, 0x8d, 0x11, 0x91,
0x1a, 0x48, 0x88, 0xf8, 0xf4, 0x82, 0xfa, 0x1c, 0x7d, 0x58, 0x74, 0xf1, 0xd2, 0x50, 0x41, 0xf2,
0xd5, 0x14, 0x65, 0x3e, 0x57, 0x1c, 0x50, 0x23, 0x82, 0x9c, 0x0c, 0x73, 0x2c, 0x30, 0x27, 0x2c,
0x09, 0xd3, 0x65, 0xc3, 0x3f, 0x42, 0x35, 0xfb, 0x9d, 0x3d, 0xe3, 0x33, 0xd8, 0x09, 0x7d, 0xae,
0x9c, 0x46, 0xc3, 0x68, 0xc1, 0x45, 0x56, 0x7f, 0x35, 0xf4, 0xb9, 0xec, 0x56, 0x07, 0x41, 0xe4,
0x2d, 0x1d, 0x99, 0xf1, 0xb6, 0x32, 0x9e, 0x32, 0xa5, 0xe2, 0x1d, 0xe9, 0x86, 0x66, 0x17, 0x8e,
0x74, 0xa3, 0x60, 0x17, 0x8f, 0x74, 0xa3, 0x68, 0xeb, 0x47, 0xba, 0xa1, 0xdb, 0xa5, 0x23, 0xdd,
0x28, 0xdb, 0x86, 0x33, 0x07, 0xf3, 0x87, 0x45, 0x24, 0xd8, 0xfd, 0xce, 0xc7, 0xce, 0xdc, 0xce,
0x5f, 0x01, 0xe7, 0x0f, 0xa6, 0xb7, 0xa3, 0xb7, 0x61, 0xd6, 0xe2, 0x27, 0xcc, 0xfa, 0x87, 0x06,
0xd5, 0x2c, 0x50, 0x56, 0xe7, 0x01, 0x18, 0xab, 0x69, 0x52, 0xe1, 0xca, 0x69, 0x36, 0x4a, 0x8f,
0x01, 0x72, 0x8b, 0x46, 0x8d, 0x5a, 0x25, 0x5e, 0x6d, 0x99, 0xff, 0x41, 0xe5, 0xee, 0x94, 0x19,
0xe1, 0x72, 0xc4, 0x70, 0x69, 0xc8, 0x45, 0x40, 0x6f, 0x42, 0xc6, 0x85, 0x87, 0x1b, 0x55, 0x9a,
0xce, 0x94, 0x4b, 0x83, 0xc6, 0x23, 0x85, 0xf7, 0x64, 0xb1, 0x8f, 0x01, 0xa6, 0x81, 0xb8, 0xf4,
0x66, 0x2c, 0x10, 0x14, 0xbb, 0x5c, 0x72, 0x2b, 0x12, 0xe9, 0x49, 0xe0, 0xf0, 0x29, 0x18, 0x4b,
0x17, 0x13, 0x13, 0x8c, 0xe3, 0xe1, 0x70, 0xe4, 0x0d, 0xcf, 0x26, 0xf6, 0x03, 0xb2, 0x0d, 0x65,
0xfc, 0x35, 0x38, 0xb5, 0xb5, 0xc3, 0x14, 0x2a, 0x2b, 0x13, 0x93, 0x2a, 0x54, 0x06, 0xa7, 0x83,
0xc9, 0xa0, 0x33, 0xe9, 0xf7, 0xec, 0x07, 0xe4, 0x11, 0xd4, 0x46, 0x6e, 0x7f, 0x70, 0xd2, 0x79,
0xd3, 0xf7, 0xdc, 0xfe, 0xbb, 0x7e, 0xe7, 0xb8, 0xdf, 0xb3, 0x35, 0x42, 0xc0, 0x7a, 0x3b, 0x39,
0xee, 0x7a, 0xa3, 0xb3, 0xef, 0x8f, 0x07, 0xe3, 0xb7, 0xfd, 0x9e, 0x5d, 0x90, 0x9a, 0xe3, 0xb3,
0x6e, 0xb7, 0x3f, 0x1e, 0xdb, 0x45, 0x02, 0xb0, 0xf5, 0xba, 0x33, 0x90, 0x64, 0x9d, 0xec, 0xc2,
0xce, 0xe0, 0xf4, 0xdd, 0x70, 0xd0, 0xed, 0x7b, 0xe3, 0xfe, 0x64, 0x22, 0xc1, 0xd2, 0x8b, 0x7f,
0x75, 0x35, 0xa7, 0x5d, 0xfc, 0x18, 0x11, 0x17, 0xca, 0xd9, 0xe7, 0x85, 0xec, 0xaf, 0x46, 0x6b,
0xfd, 0x83, 0xd3, 0x78, 0xb4, 0x36, 0x73, 0xcb, 0x77, 0x70, 0xf6, 0x7f, 0xfd, 0xeb, 0x9f, 0xdf,
0x0a, 0x35, 0xc7, 0x6c, 0x5f, 0x7e, 0xdb, 0x96, 0x8c, 0x76, 0xb4, 0x10, 0xaf, 0xb4, 0x43, 0x32,
0x84, 0x2d, 0xb5, 0x15, 0xc9, 0xde, 0x9a, 0xe4, 0x6a, 0x4d, 0xde, 0xa7, 0xb8, 0x87, 0x8a, 0xb6,
0xb3, 0xbd, 0x52, 0xf4, 0xb9, 0x14, 0xfc, 0x0e, 0xca, 0xd9, 0xb6, 0xc9, 0x25, 0xb9, 0xbe, 0x7f,
0x1a, 0xbb, 0x1b, 0x8b, 0x61, 0x91, 0x7e, 0xa3, 0x91, 0x1f, 0xc1, 0xcc, 0xaa, 0xc1, 0x61, 0x21,
0xb7, 0x91, 0xf3, 0xc3, 0xd4, 0xd8, 0xbb, 0x0b, 0x67, 0x19, 0x35, 0x30, 0xa3, 0x87, 0x84, 0xe4,
0x6b, 0x6c, 0x0b, 0x94, 0xf2, 0x56, 0xd2, 0xe8, 0xcf, 0x9c, 0x74, 0x7e, 0x30, 0x72, 0xd2, 0x6b,
0x36, 0x76, 0x9a, 0x28, 0xdd, 0x20, 0xf5, 0x35, 0xe9, 0x0f, 0x92, 0xd3, 0xfe, 0x85, 0x86, 0xe2,
0x23, 0xf9, 0x09, 0xac, 0x37, 0x4c, 0xa8, 0xce, 0x7d, 0x56, 0xf6, 0x07, 0x18, 0x62, 0x97, 0xd4,
0x72, 0xfd, 0xcc, 0x92, 0xff, 0x39, 0xa7, 0xfd, 0x59, 0xe9, 0x3f, 0x41, 0xed, 0x03, 0xb2, 0x9f,
0xd7, 0xce, 0x65, 0xff, 0x7e, 0x0b, 0xff, 0x81, 0x79, 0xf9, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff,
0xc2, 0x6b, 0xe2, 0x2f, 0xf7, 0x08, 0x00, 0x00,
// 1310 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0xcd, 0x72, 0x1a, 0x47,
0x10, 0x36, 0x3f, 0x12, 0xd0, 0x2c, 0xcb, 0x6a, 0x64, 0x4b, 0x48, 0x89, 0xcb, 0x78, 0x13, 0x3b,
0x44, 0x07, 0x13, 0xdb, 0xa7, 0xb8, 0x72, 0x21, 0x08, 0x5b, 0xa8, 0x24, 0x41, 0x16, 0xe4, 0x2a,
0xe7, 0xb2, 0x19, 0xc3, 0x48, 0xda, 0x0a, 0x3b, 0xbb, 0xde, 0x19, 0x6c, 0xa9, 0x52, 0xbe, 0xe4,
0x15, 0x72, 0xcb, 0x5b, 0xa4, 0x2a, 0xb7, 0x9c, 0xf2, 0x0c, 0xb9, 0xe7, 0x94, 0x07, 0x49, 0x4d,
0xcf, 0xb0, 0x5a, 0x84, 0x7d, 0xf1, 0x8d, 0xfd, 0xe6, 0x9b, 0x6f, 0xba, 0x7b, 0xbe, 0xee, 0x01,
0xac, 0xc9, 0x2c, 0x60, 0x5c, 0x3e, 0x8a, 0x93, 0x48, 0x46, 0xa4, 0x34, 0x8b, 0xa2, 0x38, 0x89,
0x27, 0xbb, 0x9f, 0x9f, 0x47, 0xd1, 0xf9, 0x8c, 0xb5, 0x69, 0x1c, 0xb4, 0x29, 0xe7, 0x91, 0xa4,
0x32, 0x88, 0xb8, 0xd0, 0x34, 0xf7, 0xf7, 0x02, 0xd8, 0x47, 0x51, 0x14, 0x0f, 0xe6, 0xd2, 0x63,
0x6f, 0xe6, 0x4c, 0x48, 0xe2, 0x40, 0x81, 0x86, 0xb2, 0x91, 0x6b, 0xe6, 0x5a, 0x05, 0x4f, 0xfd,
0x24, 0x04, 0x8a, 0x53, 0x26, 0x64, 0x23, 0xdf, 0xcc, 0xb5, 0x2a, 0x1e, 0xfe, 0x26, 0x6d, 0xb8,
0x1d, 0xd2, 0x4b, 0x5f, 0xbc, 0xa3, 0xb1, 0x9f, 0x44, 0x73, 0x19, 0xf0, 0x73, 0xff, 0x8c, 0xb1,
0x46, 0x01, 0xb7, 0x6d, 0x84, 0xf4, 0x72, 0xf4, 0x8e, 0xc6, 0x9e, 0x5e, 0x79, 0xce, 0x18, 0x79,
0x0a, 0x5b, 0x6a, 0x43, 0x9c, 0xb0, 0x98, 0x5e, 0x2d, 0x6d, 0x29, 0xe2, 0x96, 0xcd, 0x90, 0x5e,
0x0e, 0x71, 0x31, 0xb3, 0xa9, 0x09, 0x56, 0x7a, 0x8a, 0xa2, 0xae, 0x21, 0x15, 0x8c, 0xba, 0x62,
0x7c, 0x09, 0x76, 0x46, 0x56, 0x05, 0xbe, 0x8e, 0x1c, 0x2b, 0x95, 0xeb, 0x84, 0x92, 0xb8, 0x50,
0x53, 0xac, 0x30, 0xe0, 0x2c, 0x41, 0xa1, 0x12, 0x92, 0xaa, 0x21, 0xbd, 0x3c, 0x56, 0x98, 0x52,
0x6a, 0x81, 0xa3, 0x6a, 0xe6, 0x47, 0x73, 0xe9, 0x4f, 0x2e, 0x28, 0xe7, 0x6c, 0xd6, 0x28, 0x37,
0x73, 0xad, 0xa2, 0x67, 0xcf, 0x74, 0x85, 0xba, 0x1a, 0x25, 0x7b, 0xb0, 0x21, 0xde, 0x31, 0x16,
0xfb, 0x93, 0x88, 0x9f, 0xf9, 0x92, 0x26, 0xe7, 0x4c, 0x36, 0x2a, 0xcd, 0x5c, 0x6b, 0xcd, 0xab,
0xe3, 0x42, 0x37, 0xe2, 0x67, 0x63, 0x84, 0xc9, 0x33, 0xd8, 0xc1, 0xe8, 0xe3, 0xf9, 0xeb, 0x59,
0x30, 0xc1, 0xda, 0xfb, 0x53, 0x46, 0xa7, 0xb3, 0x80, 0xb3, 0x06, 0xa0, 0xfc, 0xb6, 0x22, 0x0c,
0xaf, 0xd7, 0xf7, 0xcd, 0xb2, 0xfb, 0x67, 0x0e, 0x6a, 0xea, 0x72, 0xfa, 0xfc, 0xe3, 0x77, 0x73,
0xb3, 0x42, 0xf9, 0x95, 0x0a, 0xad, 0xe4, 0x5e, 0x58, 0xcd, 0xfd, 0x21, 0xd4, 0x31, 0xf7, 0x80,
0xa7, 0xa9, 0x17, 0x31, 0xb6, 0xda, 0x0c, 0xcf, 0x5f, 0x64, 0xfe, 0x05, 0xd4, 0xd8, 0xa5, 0x64,
0x09, 0xa7, 0x33, 0xff, 0x42, 0xce, 0x26, 0x78, 0x21, 0x65, 0xcf, 0x5a, 0x80, 0x07, 0x72, 0x36,
0x71, 0x3b, 0x60, 0xe1, 0xdd, 0x33, 0x11, 0x47, 0x5c, 0x30, 0x62, 0x43, 0x3e, 0x98, 0x62, 0xcc,
0x15, 0x2f, 0x1f, 0x4c, 0xc9, 0x7d, 0xb0, 0xd4, 0x5e, 0x9f, 0x4e, 0xa7, 0x09, 0x13, 0xc2, 0xd8,
0xaa, 0xaa, 0xb0, 0x8e, 0x86, 0x5c, 0x07, 0xec, 0xe3, 0x88, 0x07, 0x32, 0x4a, 0x4c, 0xe6, 0xee,
0xbf, 0x79, 0x00, 0xa5, 0x3a, 0x92, 0x54, 0xce, 0xc5, 0x07, 0x0a, 0xa1, 0x4f, 0xc9, 0xa7, 0xa7,
0x3c, 0x80, 0xa2, 0xbc, 0x8a, 0x75, 0xb6, 0xf6, 0x93, 0x8d, 0x47, 0xa6, 0x1f, 0x1e, 0x29, 0x91,
0xf1, 0x55, 0xcc, 0x3c, 0x5c, 0x26, 0x2d, 0x58, 0x13, 0x92, 0x4a, 0xed, 0x42, 0xfb, 0x09, 0x59,
0xe2, 0xa9, 0xc3, 0x98, 0xa7, 0x09, 0xe4, 0x2b, 0xa8, 0x07, 0x3c, 0x90, 0x81, 0xbe, 0x43, 0x19,
0x84, 0x0b, 0x3b, 0xda, 0xd7, 0xf0, 0x38, 0x08, 0xb5, 0x91, 0xa8, 0x90, 0xfe, 0x3c, 0x9e, 0x52,
0xc9, 0x34, 0x53, 0x9b, 0xd2, 0x56, 0xf8, 0x29, 0xc2, 0xc8, 0xbc, 0x59, 0x89, 0xd2, 0x4a, 0x25,
0xc8, 0x3d, 0xa8, 0x4e, 0x22, 0x21, 0x7d, 0xc1, 0x92, 0xb7, 0x2c, 0x41, 0x43, 0x16, 0x3c, 0x50,
0xd0, 0x08, 0x11, 0xa5, 0x81, 0x84, 0x88, 0x4f, 0x2e, 0x68, 0xc0, 0xd1, 0x87, 0x05, 0x0f, 0x37,
0x0d, 0x34, 0xa4, 0x6e, 0x4d, 0x53, 0xce, 0xce, 0x34, 0x07, 0x74, 0x8b, 0x20, 0xc7, 0x60, 0xae,
0x0d, 0xd6, 0x98, 0x25, 0xa1, 0x58, 0x14, 0xfc, 0x3d, 0xd4, 0xcc, 0xb7, 0xb9, 0xc6, 0x87, 0x50,
0x0f, 0x03, 0xae, 0x9d, 0x46, 0xc3, 0x68, 0xce, 0xa5, 0xc9, 0xbf, 0x16, 0x06, 0x5c, 0x55, 0xab,
0x83, 0x20, 0xf2, 0x16, 0x8e, 0x34, 0xbc, 0x75, 0xc3, 0xd3, 0xa6, 0xd4, 0xbc, 0xc3, 0x62, 0x39,
0xe7, 0xe4, 0x0f, 0x8b, 0xe5, 0xbc, 0x53, 0x38, 0x2c, 0x96, 0x0b, 0x4e, 0xf1, 0xb0, 0x58, 0x2e,
0x3a, 0x6b, 0x87, 0xc5, 0x72, 0xc9, 0x29, 0xbb, 0x67, 0x60, 0xfd, 0x30, 0x8f, 0x24, 0xfb, 0xb8,
0xf3, 0xb1, 0x32, 0xd7, 0xfd, 0x97, 0xc7, 0xfe, 0x83, 0xc9, 0x75, 0xeb, 0xad, 0x98, 0xb5, 0xf0,
0x01, 0xb3, 0xfe, 0x91, 0x83, 0x9a, 0x39, 0xc8, 0xe4, 0xb9, 0x03, 0xe5, 0xb4, 0x9b, 0xf4, 0x71,
0x25, 0x61, 0x5a, 0xe9, 0x2e, 0x40, 0x66, 0xd0, 0xe8, 0x56, 0xab, 0xc4, 0xe9, 0x94, 0xf9, 0x0c,
0x2a, 0x37, 0xbb, 0xac, 0x1c, 0x2e, 0x5a, 0x0c, 0x87, 0x86, 0x1a, 0x04, 0xf4, 0x2a, 0x64, 0x5c,
0xfa, 0x38, 0x51, 0x95, 0xe9, 0x2c, 0x35, 0x34, 0x68, 0x3c, 0xd4, 0xf8, 0xbe, 0x4a, 0xf6, 0x2e,
0xc0, 0x64, 0x26, 0xdf, 0xfa, 0x53, 0x36, 0x93, 0x14, 0xab, 0xbc, 0xe6, 0x55, 0x14, 0xb2, 0xaf,
0x00, 0xb7, 0x0e, 0xb5, 0x71, 0xf4, 0x33, 0xe3, 0xe9, 0x5d, 0x7d, 0x07, 0xf6, 0x02, 0x30, 0x49,
0xec, 0xc1, 0xba, 0x44, 0xa4, 0x91, 0x6b, 0x16, 0x5a, 0xd5, 0x8c, 0xaf, 0x8f, 0x04, 0x95, 0x48,
0xf6, 0x0c, 0xc3, 0xfd, 0x2b, 0x0f, 0x95, 0x14, 0x55, 0x55, 0x7b, 0x4d, 0x05, 0xf3, 0x43, 0x3a,
0xa1, 0x49, 0x14, 0x71, 0xac, 0x81, 0xe5, 0x59, 0x0a, 0x3c, 0x36, 0x98, 0x32, 0xdd, 0x22, 0x8f,
0x0b, 0x2a, 0x2e, 0xb0, 0x14, 0x96, 0x57, 0x35, 0xd8, 0x01, 0x15, 0x17, 0xe4, 0x6b, 0x70, 0x16,
0x94, 0x38, 0x61, 0x41, 0x48, 0xcf, 0x75, 0x4d, 0x2c, 0xaf, 0x6e, 0xf0, 0xa1, 0x81, 0x55, 0xc3,
0x68, 0xa3, 0xf8, 0x31, 0x0d, 0xa6, 0x7e, 0x28, 0xa8, 0x34, 0x8f, 0x82, 0xad, 0xf1, 0x21, 0x0d,
0xa6, 0xc7, 0x82, 0x4a, 0xf2, 0x18, 0xee, 0x64, 0x5e, 0x8e, 0x0c, 0x5d, 0x3b, 0x91, 0x24, 0xe9,
0xd3, 0x91, 0x6e, 0xb9, 0x0f, 0x96, 0xea, 0x40, 0x7f, 0x92, 0x30, 0x2a, 0xd9, 0xd4, 0x78, 0xb1,
0xaa, 0xb0, 0xae, 0x86, 0x48, 0x03, 0x4a, 0xec, 0x32, 0x0e, 0x12, 0x36, 0xc5, 0x0e, 0x2c, 0x7b,
0x8b, 0x4f, 0xb5, 0x59, 0xc8, 0x28, 0xa1, 0xe7, 0xcc, 0xe7, 0x34, 0x64, 0xd8, 0x7e, 0x15, 0xaf,
0x6a, 0xb0, 0x13, 0x1a, 0xb2, 0xbd, 0x07, 0x50, 0x5e, 0x8c, 0x14, 0x62, 0x41, 0xf9, 0x68, 0x30,
0x18, 0xfa, 0x83, 0xd3, 0xb1, 0x73, 0x8b, 0x54, 0xa1, 0x84, 0x5f, 0xfd, 0x13, 0x27, 0xb7, 0x27,
0xa0, 0x92, 0x4e, 0x14, 0x52, 0x83, 0x4a, 0xff, 0xa4, 0x3f, 0xee, 0x77, 0xc6, 0xbd, 0x7d, 0xe7,
0x16, 0xb9, 0x03, 0x1b, 0x43, 0xaf, 0xd7, 0x3f, 0xee, 0xbc, 0xe8, 0xf9, 0x5e, 0xef, 0x65, 0xaf,
0x73, 0xd4, 0xdb, 0x77, 0x72, 0x84, 0x80, 0x7d, 0x30, 0x3e, 0xea, 0xfa, 0xc3, 0xd3, 0xef, 0x8f,
0xfa, 0xa3, 0x83, 0xde, 0xbe, 0x93, 0x57, 0x9a, 0xa3, 0xd3, 0x6e, 0xb7, 0x37, 0x1a, 0x39, 0x05,
0x02, 0xb0, 0xfe, 0xbc, 0xd3, 0x57, 0xe4, 0x22, 0xd9, 0x84, 0x7a, 0xff, 0xe4, 0xe5, 0xa0, 0xdf,
0xed, 0xf9, 0xa3, 0xde, 0x78, 0xac, 0xc0, 0xb5, 0x27, 0x7f, 0xaf, 0xe9, 0xa1, 0xd9, 0xc5, 0x7f,
0x06, 0xc4, 0x83, 0x92, 0x79, 0xeb, 0xc9, 0xf6, 0xb5, 0x1f, 0x96, 0x5e, 0xff, 0xdd, 0x3b, 0x4b,
0x03, 0x70, 0xe1, 0x27, 0x77, 0xfb, 0xd7, 0x7f, 0xfe, 0xfb, 0x2d, 0xbf, 0xe1, 0x5a, 0xed, 0xb7,
0x8f, 0xdb, 0x8a, 0xd1, 0x8e, 0xe6, 0xf2, 0x59, 0x6e, 0x8f, 0x0c, 0x60, 0x5d, 0x3f, 0x51, 0x64,
0x6b, 0x49, 0x32, 0x7d, 0xb3, 0x3e, 0xa6, 0xb8, 0x85, 0x8a, 0x8e, 0x5b, 0x4d, 0x15, 0x03, 0xae,
0x04, 0xbf, 0x85, 0x92, 0x19, 0xfd, 0x99, 0x20, 0x97, 0x1f, 0x83, 0xdd, 0xcd, 0x95, 0x29, 0x3d,
0x17, 0xdf, 0xe4, 0xc8, 0x2b, 0xb0, 0x4c, 0x36, 0x38, 0xb9, 0xc8, 0xf5, 0xc9, 0xd9, 0xc9, 0xb6,
0xbb, 0x75, 0x13, 0x36, 0x11, 0xed, 0x62, 0x44, 0xb7, 0x09, 0xc9, 0xe6, 0xd8, 0x96, 0x28, 0xe5,
0xa7, 0xd2, 0x38, 0x2c, 0x32, 0xd2, 0xd9, 0x29, 0x95, 0x91, 0x5e, 0x9a, 0x29, 0x6e, 0x13, 0xa5,
0x77, 0x49, 0x63, 0x49, 0xfa, 0x8d, 0xe2, 0xb4, 0x7f, 0xa1, 0xa1, 0x7c, 0x4f, 0x7e, 0x04, 0xfb,
0x05, 0x93, 0xba, 0x72, 0x9f, 0x14, 0xfd, 0x0e, 0x1e, 0xb1, 0x49, 0x36, 0x32, 0xf5, 0x34, 0xc1,
0xff, 0x94, 0xd1, 0xfe, 0xa4, 0xf0, 0xef, 0xa1, 0xf6, 0x0e, 0xd9, 0xce, 0x6a, 0x67, 0xa3, 0x7f,
0x05, 0x35, 0x75, 0xc2, 0x62, 0x88, 0x88, 0x8c, 0x19, 0x96, 0x26, 0xd5, 0xee, 0xf6, 0x0a, 0xbe,
0x6c, 0x30, 0x52, 0xc7, 0x23, 0x04, 0x95, 0x6d, 0x3d, 0x9d, 0x5e, 0xaf, 0xe3, 0x1f, 0xd5, 0xa7,
0xff, 0x07, 0x00, 0x00, 0xff, 0xff, 0xbc, 0xc2, 0x52, 0x9d, 0xdf, 0x0a, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@ -931,6 +1133,9 @@ type SwapClientClient interface {
//*
//GetQuote returns a quote for a swap with the provided parameters.
GetLoopInQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error)
//*
//GetLsatTokens returns all LSAT tokens the daemon ever paid for.
GetLsatTokens(ctx context.Context, in *TokensRequest, opts ...grpc.CallOption) (*TokensResponse, error)
}
type swapClientClient struct {
@ -1027,6 +1232,15 @@ func (c *swapClientClient) GetLoopInQuote(ctx context.Context, in *QuoteRequest,
return out, nil
}
func (c *swapClientClient) GetLsatTokens(ctx context.Context, in *TokensRequest, opts ...grpc.CallOption) (*TokensResponse, error) {
out := new(TokensResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/GetLsatTokens", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SwapClientServer is the server API for SwapClient service.
type SwapClientServer interface {
//* loop: `out`
@ -1058,6 +1272,9 @@ type SwapClientServer interface {
//*
//GetQuote returns a quote for a swap with the provided parameters.
GetLoopInQuote(context.Context, *QuoteRequest) (*QuoteResponse, error)
//*
//GetLsatTokens returns all LSAT tokens the daemon ever paid for.
GetLsatTokens(context.Context, *TokensRequest) (*TokensResponse, error)
}
func RegisterSwapClientServer(s *grpc.Server, srv SwapClientServer) {
@ -1193,6 +1410,24 @@ func _SwapClient_GetLoopInQuote_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler)
}
func _SwapClient_GetLsatTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TokensRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).GetLsatTokens(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/GetLsatTokens",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).GetLsatTokens(ctx, req.(*TokensRequest))
}
return interceptor(ctx, in, info, handler)
}
var _SwapClient_serviceDesc = grpc.ServiceDesc{
ServiceName: "looprpc.SwapClient",
HandlerType: (*SwapClientServer)(nil),
@ -1221,6 +1456,10 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{
MethodName: "GetLoopInQuote",
Handler: _SwapClient_GetLoopInQuote_Handler,
},
{
MethodName: "GetLsatTokens",
Handler: _SwapClient_GetLsatTokens_Handler,
},
},
Streams: []grpc.StreamDesc{
{

@ -9,13 +9,13 @@ It translates gRPC into RESTful JSON APIs.
package looprpc
import (
"context"
"io"
"net/http"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
@ -32,7 +32,11 @@ func request_SwapClient_LoopOut_0(ctx context.Context, marshaler runtime.Marshal
var protoReq LoopOutRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil {
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
@ -45,7 +49,11 @@ func request_SwapClient_LoopIn_0(ctx context.Context, marshaler runtime.Marshale
var protoReq LoopInRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil {
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
@ -142,6 +150,15 @@ func request_SwapClient_GetLoopInQuote_0(ctx context.Context, marshaler runtime.
}
func request_SwapClient_GetLsatTokens_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TokensRequest
var metadata runtime.ServerMetadata
msg, err := client.GetLsatTokens(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
// RegisterSwapClientHandlerFromEndpoint is same as RegisterSwapClientHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterSwapClientHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
@ -152,14 +169,14 @@ func RegisterSwapClientHandlerFromEndpoint(ctx context.Context, mux *runtime.Ser
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
@ -170,20 +187,19 @@ func RegisterSwapClientHandlerFromEndpoint(ctx context.Context, mux *runtime.Ser
// RegisterSwapClientHandler registers the http handlers for service SwapClient to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
client := NewSwapClientClient(conn)
return RegisterSwapClientHandlerClient(ctx, mux, NewSwapClientClient(conn))
}
// RegisterSwapClientHandlerClient registers the http handlers for service SwapClient
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "SwapClientClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "SwapClientClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "SwapClientClient" to call the correct interceptors.
func RegisterSwapClientHandlerClient(ctx context.Context, mux *runtime.ServeMux, client SwapClientClient) error {
mux.Handle("POST", pattern_SwapClient_LoopOut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
@ -202,17 +218,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn
})
mux.Handle("POST", pattern_SwapClient_LoopIn_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
@ -231,17 +238,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn
})
mux.Handle("GET", pattern_SwapClient_LoopOutTerms_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
@ -260,17 +258,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn
})
mux.Handle("GET", pattern_SwapClient_LoopOutQuote_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
@ -289,17 +278,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn
})
mux.Handle("GET", pattern_SwapClient_GetLoopInTerms_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
@ -318,17 +298,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn
})
mux.Handle("GET", pattern_SwapClient_GetLoopInQuote_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
@ -346,6 +317,26 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn
})
mux.Handle("GET", pattern_SwapClient_GetLsatTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_SwapClient_GetLsatTokens_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_SwapClient_GetLsatTokens_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@ -361,6 +352,8 @@ var (
pattern_SwapClient_GetLoopInTerms_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "loop", "in", "terms"}, ""))
pattern_SwapClient_GetLoopInQuote_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"v1", "loop", "in", "quote", "amt"}, ""))
pattern_SwapClient_GetLsatTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "lsat", "tokens"}, ""))
)
var (
@ -375,4 +368,6 @@ var (
forward_SwapClient_GetLoopInTerms_0 = runtime.ForwardResponseMessage
forward_SwapClient_GetLoopInQuote_0 = runtime.ForwardResponseMessage
forward_SwapClient_GetLsatTokens_0 = runtime.ForwardResponseMessage
)

@ -15,7 +15,7 @@ service SwapClient {
point onwards, progress can be tracked via the SwapStatus stream that is
returned from Monitor().
*/
rpc LoopOut(LoopOutRequest) returns (SwapResponse) {
rpc LoopOut (LoopOutRequest) returns (SwapResponse) {
option (google.api.http) = {
post: "/v1/loop/out"
body: "*"
@ -28,7 +28,7 @@ service SwapClient {
point onwards, progress can be tracked via the SwapStatus stream
that is returned from Monitor().
*/
rpc LoopIn(LoopInRequest) returns (SwapResponse) {
rpc LoopIn (LoopInRequest) returns (SwapResponse) {
option (google.api.http) = {
post: "/v1/loop/in"
body: "*"
@ -39,12 +39,12 @@ service SwapClient {
Monitor will return a stream of swap updates for currently active swaps.
TODO: add MonitorSync version for REST clients.
*/
rpc Monitor(MonitorRequest) returns(stream SwapStatus);
rpc Monitor (MonitorRequest) returns (stream SwapStatus);
/** loop: `terms`
LoopOutTerms returns the terms that the server enforces for a loop out swap.
*/
rpc LoopOutTerms(TermsRequest) returns(TermsResponse) {
rpc LoopOutTerms (TermsRequest) returns (TermsResponse) {
option (google.api.http) = {
get: "/v1/loop/out/terms"
};
@ -54,7 +54,7 @@ service SwapClient {
LoopOutQuote returns a quote for a loop out swap with the provided
parameters.
*/
rpc LoopOutQuote(QuoteRequest) returns(QuoteResponse) {
rpc LoopOutQuote (QuoteRequest) returns (QuoteResponse) {
option (google.api.http) = {
get: "/v1/loop/out/quote/{amt}"
};
@ -63,7 +63,7 @@ service SwapClient {
/**
GetTerms returns the terms that the server enforces for swaps.
*/
rpc GetLoopInTerms(TermsRequest) returns(TermsResponse) {
rpc GetLoopInTerms (TermsRequest) returns (TermsResponse) {
option (google.api.http) = {
get: "/v1/loop/in/terms"
};
@ -72,11 +72,20 @@ service SwapClient {
/**
GetQuote returns a quote for a swap with the provided parameters.
*/
rpc GetLoopInQuote(QuoteRequest) returns(QuoteResponse) {
rpc GetLoopInQuote (QuoteRequest) returns (QuoteResponse) {
option (google.api.http) = {
get: "/v1/loop/in/quote/{amt}"
};
}
/**
GetLsatTokens returns all LSAT tokens the daemon ever paid for.
*/
rpc GetLsatTokens (TokensRequest) returns (TokensResponse) {
option (google.api.http) = {
get: "/v1/lsat/tokens"
};
}
}
message LoopOutRequest {
@ -180,7 +189,7 @@ message LoopInRequest {
max_miner_fee is typically taken from the response of the GetQuote call.
*/
int64 max_miner_fee = 3;
/**
The channel to loop in. If zero, the channel to loop in is selected based
on the lowest routing fee for the swap payment from the server.
@ -209,7 +218,7 @@ message SwapResponse {
string htlc_address = 2;
}
message MonitorRequest{
message MonitorRequest {
}
message SwapStatus {
@ -303,10 +312,10 @@ enum SwapState {
*/
FAILED = 4;
/**
INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been
paid, but we are still waiting for the htlc spend to confirm.
*/
/**
INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been
paid, but we are still waiting for the htlc spend to confirm.
*/
INVOICE_SETTLED = 5;
}
@ -315,7 +324,7 @@ message TermsRequest {
message TermsResponse {
reserved 1,2,3,4,7;
reserved 1, 2, 3, 4, 7;
/**
Minimum swap amount (sat)
@ -376,3 +385,58 @@ message QuoteResponse {
*/
int32 cltv_delta = 5;
}
message TokensRequest {
}
message TokensResponse {
/**
List of all tokens the daemon knows of, including old/expired tokens.
*/
repeated LsatToken tokens = 1;
}
message LsatToken {
/**
The base macaroon that was baked by the auth server.
*/
bytes base_macaroon = 1;
/**
The payment hash of the payment that was paid to obtain the token.
*/
bytes payment_hash = 2;
/**
The preimage of the payment hash, knowledge of this is proof that the
payment has been paid. If the preimage is set to all zeros, this means the
payment is still pending and the token is not yet fully valid.
*/
bytes payment_preimage = 3;
/**
The amount of millisatoshis that was paid to get the token.
*/
int64 amount_paid_msat = 4;
/**
The amount of millisatoshis paid in routing fee to pay for the token.
*/
int64 routing_fee_paid_msat = 5;
/**
The creation time of the token as UNIX timestamp in seconds.
*/
int64 time_created = 6;
/**
Indicates whether the token is expired or still valid.
*/
bool expired = 7;
/**
Identifying attribute of this token in the store. Currently represents the
file name of the token where it's stored on the file system.
*/
string storage_name = 8;
}

@ -21,7 +21,7 @@
"operationId": "LoopIn",
"responses": {
"200": {
"description": "",
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/looprpcSwapResponse"
}
@ -48,7 +48,7 @@
"operationId": "GetLoopInQuote",
"responses": {
"200": {
"description": "",
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/looprpcQuoteResponse"
}
@ -57,6 +57,7 @@
"parameters": [
{
"name": "amt",
"description": "*\nThe amount to swap in satoshis.",
"in": "path",
"required": true,
"type": "string",
@ -90,7 +91,7 @@
"operationId": "GetLoopInTerms",
"responses": {
"200": {
"description": "",
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/looprpcTermsResponse"
}
@ -107,7 +108,7 @@
"operationId": "LoopOut",
"responses": {
"200": {
"description": "",
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/looprpcSwapResponse"
}
@ -134,7 +135,7 @@
"operationId": "LoopOutQuote",
"responses": {
"200": {
"description": "",
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/looprpcQuoteResponse"
}
@ -143,6 +144,7 @@
"parameters": [
{
"name": "amt",
"description": "*\nThe amount to swap in satoshis.",
"in": "path",
"required": true,
"type": "string",
@ -176,7 +178,7 @@
"operationId": "LoopOutTerms",
"responses": {
"200": {
"description": "",
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/looprpcTermsResponse"
}
@ -186,6 +188,23 @@
"SwapClient"
]
}
},
"/v1/lsat/tokens": {
"get": {
"summary": "*\nGetLsatTokens returns all LSAT tokens the daemon ever paid for.",
"operationId": "GetLsatTokens",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/looprpcTokensResponse"
}
}
},
"tags": [
"SwapClient"
]
}
}
},
"definitions": {
@ -273,6 +292,50 @@
}
}
},
"looprpcLsatToken": {
"type": "object",
"properties": {
"base_macaroon": {
"type": "string",
"format": "byte",
"description": "*\nThe base macaroon that was baked by the auth server."
},
"payment_hash": {
"type": "string",
"format": "byte",
"description": "*\nThe payment hash of the payment that was paid to obtain the token."
},
"payment_preimage": {
"type": "string",
"format": "byte",
"description": "*\nThe preimage of the payment hash, knowledge of this is proof that the\npayment has been paid. If the preimage is set to all zeros, this means the\npayment is still pending and the token is not yet fully valid."
},
"amount_paid_msat": {
"type": "string",
"format": "int64",
"description": "*\nThe amount of millisatoshis that was paid to get the token."
},
"routing_fee_paid_msat": {
"type": "string",
"format": "int64",
"description": "*\nThe amount of millisatoshis paid in routing fee to pay for the token."
},
"time_created": {
"type": "string",
"format": "int64",
"description": "*\nThe creation time of the token as UNIX timestamp in seconds."
},
"expired": {
"type": "boolean",
"format": "boolean",
"description": "*\nIndicates whether the token is expired or still valid."
},
"storage_name": {
"type": "string",
"description": "*\nIdentifying attribute of this token in the store. Currently represents the\nfile name of the token where it's stored on the file system."
}
}
},
"looprpcQuoteResponse": {
"type": "object",
"properties": {
@ -403,6 +466,69 @@
"title": "*\nMaximum swap amount (sat)"
}
}
},
"looprpcTokensResponse": {
"type": "object",
"properties": {
"tokens": {
"type": "array",
"items": {
"$ref": "#/definitions/looprpcLsatToken"
},
"description": "*\nList of all tokens the daemon knows of, including old/expired tokens."
}
}
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"looprpcSwapStatus": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/looprpcSwapStatus"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of looprpcSwapStatus"
}
}
}

@ -0,0 +1,340 @@
package lsat
import (
"context"
"encoding/base64"
"fmt"
"regexp"
"sync"
"time"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/macaroons"
"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 is the error code we receive from a gRPC call if the
// server expects a payment.
GRPCErrCode = codes.Internal
// GRPCErrMessage is the error message we receive from a gRPC call in
// conjunction with the GRPCErrCode to signal the client that a payment
// is required to access the service.
GRPCErrMessage = "payment required"
// AuthHeader is is the HTTP response header that contains the payment
// challenge.
AuthHeader = "WWW-Authenticate"
// MaxRoutingFee is the maximum routing fee in satoshis that we are
// going to pay to acquire an LSAT token.
// TODO(guggero): make this configurable
MaxRoutingFeeSats = 10
// PaymentTimeout is the maximum time we allow a payment to take before
// we stop waiting for it.
PaymentTimeout = 60 * time.Second
// manualRetryHint is the error text we return to tell the user how a
// token payment can be retried if the payment fails.
manualRetryHint = "consider removing pending token file if error " +
"persists. use 'listauth' command to find out token file name"
)
var (
// authHeaderRegex is the regular expression the payment challenge must
// match for us to be able to parse the macaroon and invoice.
authHeaderRegex = regexp.MustCompile(
"LSAT macaroon='(.*?)' invoice='(.*?)'",
)
)
// Interceptor is a gRPC client interceptor that can handle LSAT authentication
// challenges with embedded payment requests. It uses a connection to lnd to
// automatically pay for an authentication token.
type Interceptor struct {
lnd *lndclient.LndServices
store Store
callTimeout time.Duration
lock sync.Mutex
}
// NewInterceptor creates a new gRPC client interceptor that uses the provided
// lnd connection to automatically acquire and pay for LSAT tokens, unless the
// indicated store already contains a usable token.
func NewInterceptor(lnd *lndclient.LndServices, store Store,
rpcCallTimeout time.Duration) *Interceptor {
return &Interceptor{
lnd: lnd,
store: store,
callTimeout: rpcCallTimeout,
}
}
// UnaryInterceptor is an interceptor method that can be used directly by gRPC
// for unary calls. If the store contains a token, it is attached as credentials
// to every call before patching it through. The response error is also
// intercepted for every call. If there is an error returned and it is
// indicating a payment challenge, a token is acquired and paid for
// automatically. The original request is then repeated back to the server, now
// with the new token attached.
func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string,
req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
opts ...grpc.CallOption) error {
// To avoid paying for a token twice if two parallel requests are
// happening, we require an exclusive lock here.
i.lock.Lock()
defer i.lock.Unlock()
addLsatCredentials := func(token *Token) error {
macaroon, err := token.PaidMacaroon()
if err != nil {
return err
}
opts = append(opts, grpc.PerRPCCredentials(
macaroons.NewMacaroonCredential(macaroon),
))
return nil
}
// Let's see if the store already contains a token and what state it
// might be in. If a previous call was aborted, we might have a pending
// token that needs to be handled separately.
token, err := i.store.CurrentToken()
switch {
// If there is no token yet, nothing to do at this point.
case err == ErrNoToken:
// Some other error happened that we have to surface.
case err != nil:
log.Errorf("Failed to get token from store: %v", err)
return fmt.Errorf("getting token from store failed: %v", err)
// Only if we have a paid token append it. We don't resume a pending
// payment just yet, since we don't even know if a token is required for
// this call. We also never send a pending payment to the server since
// we know it's not valid.
case !token.isPending():
if err = addLsatCredentials(token); err != nil {
log.Errorf("Adding macaroon to request failed: %v", err)
return fmt.Errorf("adding macaroon failed: %v", err)
}
}
// We need a way to extract the response headers sent by the server.
// This can only be done through the experimental grpc.Trailer call
// option. We execute the request and inspect the error. If it's the
// LSAT specific payment required error, we might execute the same
// method again later with the paid LSAT token.
trailerMetadata := &metadata.MD{}
opts = append(opts, grpc.Trailer(trailerMetadata))
rpcCtx, cancel := context.WithTimeout(ctx, i.callTimeout)
defer cancel()
err = invoker(rpcCtx, method, req, reply, cc, opts...)
// Only handle the LSAT error message that comes in the form of
// a gRPC status error.
if isPaymentRequired(err) {
paidToken, err := i.handlePayment(ctx, token, trailerMetadata)
if err != nil {
return err
}
if err = addLsatCredentials(paidToken); err != nil {
log.Errorf("Adding macaroon to request failed: %v", err)
return fmt.Errorf("adding macaroon failed: %v", err)
}
// Execute the same request again, now with the LSAT
// token added as an RPC credential.
rpcCtx2, cancel2 := context.WithTimeout(ctx, i.callTimeout)
defer cancel2()
return invoker(rpcCtx2, method, req, reply, cc, opts...)
}
return err
}
// handlePayment tries to obtain a valid token by either tracking the payment
// status of a pending token or paying for a new one.
func (i *Interceptor) handlePayment(ctx context.Context, token *Token,
md *metadata.MD) (*Token, error) {
switch {
// Resume/track a pending payment if it was interrupted for some reason.
case token != nil && token.isPending():
log.Infof("Payment of LSAT token is required, resuming/" +
"tracking previous payment from pending LSAT token")
err := i.trackPayment(ctx, token)
if err != nil {
return nil, err
}
return token, nil
// We don't have a token yet, try to get a new one.
case token == nil:
// We don't have a token yet, get a new one.
log.Infof("Payment of LSAT token is required, paying invoice")
return i.payLsatToken(ctx, md)
// We have a token and it's valid, nothing more to do here.
default:
log.Debugf("Found valid LSAT token to add to request")
return token, nil
}
}
// payLsatToken reads the payment challenge from the response metadata and tries
// to pay the invoice encoded in them, returning a paid LSAT token if
// successful.
func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) (
*Token, error) {
// First parse the authentication header that was stored in the
// metadata.
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])
}
// Decode the base64 macaroon and the invoice so we can store the
// information in our store later.
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)
}
// Create and store the pending token so we can resume the payment in
// case the payment is interrupted somehow.
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)
}
// Pay invoice now and wait for the result to arrive or the main context
// being canceled.
payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
defer cancel()
respChan := i.lnd.Client.PayInvoice(
payCtx, invoiceStr, MaxRoutingFeeSats, 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)
}
}
// trackPayment tries to resume a pending payment by tracking its state and
// waiting for a conclusive result.
func (i *Interceptor) trackPayment(ctx context.Context, token *Token) error {
// Lookup state of the payment.
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)
}
// We can't wait forever, so we give the payment tracking the same
// timeout as the original payment.
payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
defer cancel()
// We'll consume status updates until we reach a conclusive state or
// reach the timeout.
for {
select {
// If we receive a state without an error, the payment has been
// initiated. Loop until the payment
case result := <-payStatusChan:
switch result.State {
// If the payment was successful, we have all the
// information we need and we can return the fully paid
// token.
case routerrpc.PaymentState_SUCCEEDED:
extractPaymentDetails(token, result)
return i.store.StoreToken(token)
// The payment is still in transit, we'll give it more
// time to complete.
case routerrpc.PaymentState_IN_FLIGHT:
// Any other state means either error or timeout.
default:
return fmt.Errorf("payment tracking failed "+
"with state %s. %s",
result.State.String(), manualRetryHint)
}
// Abort the payment execution for any error.
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)
}
}
}
// isPaymentRequired inspects an error to find out if it's the specific gRPC
// error returned by the server to indicate a payment is required to access the
// service.
func isPaymentRequired(err error) bool {
statusErr, ok := status.FromError(err)
return ok &&
statusErr.Message() == GRPCErrMessage &&
statusErr.Code() == GRPCErrCode
}
// extractPaymentDetails extracts the preimage and amounts paid for a payment
// from the payment status and stores them in the token.
func extractPaymentDetails(token *Token, status lndclient.PaymentStatus) {
token.Preimage = status.Preimage
total := status.Route.TotalAmount
fees := status.Route.TotalFees()
token.AmountPaid = total - fees
token.RoutingFeePaid = fees
}

@ -0,0 +1,329 @@
package lsat
import (
"context"
"encoding/base64"
"encoding/hex"
"fmt"
"sync"
"testing"
"time"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
"gopkg.in/macaroon.v2"
)
type mockStore struct {
token *Token
}
func (s *mockStore) CurrentToken() (*Token, error) {
if s.token == nil {
return nil, ErrNoToken
}
return s.token, nil
}
func (s *mockStore) AllTokens() (map[string]*Token, error) {
return map[string]*Token{"foo": s.token}, nil
}
func (s *mockStore) StoreToken(token *Token) error {
s.token = token
return nil
}
// TestInterceptor tests that the interceptor can handle LSAT protocol responses
// and pay the token.
func TestInterceptor(t *testing.T) {
t.Parallel()
var (
lnd = test.NewMockLnd()
store = &mockStore{}
testTimeout = 5 * time.Second
interceptor = NewInterceptor(
&lnd.LndServices, store, testTimeout,
)
testMac = makeMac(t)
testMacBytes = serializeMac(t, testMac)
testMacHex = hex.EncodeToString(testMacBytes)
paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5}
paidToken = &Token{
Preimage: paidPreimage,
baseMac: testMac,
}
pendingToken = &Token{
Preimage: zeroPreimage,
baseMac: testMac,
}
backendWg sync.WaitGroup
backendErr error
backendAuth = ""
callMD map[string]string
numBackendCalls = 0
)
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
// resetBackend is used by the test cases to define the behaviour of the
// simulated backend and reset its starting conditions.
resetBackend := func(expectedErr error, expectedAuth string) {
backendErr = expectedErr
backendAuth = expectedAuth
callMD = nil
}
testCases := []struct {
name string
initialToken *Token
resetCb func()
expectLndCall bool
sendPaymentCb func(msg test.PaymentChannelMessage)
trackPaymentCb func(msg test.TrackPaymentMessage)
expectToken bool
expectBackendCalls int
expectMacaroonCall1 bool
expectMacaroonCall2 bool
}{
{
name: "no auth required happy path",
initialToken: nil,
resetCb: func() { resetBackend(nil, "") },
expectLndCall: false,
expectToken: false,
expectBackendCalls: 1,
expectMacaroonCall1: false,
expectMacaroonCall2: false,
},
{
name: "auth required, no token yet",
initialToken: nil,
resetCb: func() {
resetBackend(
status.New(
GRPCErrCode, GRPCErrMessage,
).Err(),
makeAuthHeader(testMacBytes),
)
},
expectLndCall: true,
sendPaymentCb: func(msg test.PaymentChannelMessage) {
if len(callMD) != 0 {
t.Fatalf("unexpected call metadata: "+
"%v", callMD)
}
// 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(msg test.TrackPaymentMessage) {
t.Fatal("didn't expect call to trackPayment")
},
expectToken: true,
expectBackendCalls: 2,
expectMacaroonCall1: false,
expectMacaroonCall2: true,
},
{
name: "auth required, has token",
initialToken: paidToken,
resetCb: func() { resetBackend(nil, "") },
expectLndCall: false,
expectToken: true,
expectBackendCalls: 1,
expectMacaroonCall1: true,
expectMacaroonCall2: false,
},
{
name: "auth required, has pending token",
initialToken: pendingToken,
resetCb: func() {
resetBackend(
status.New(
GRPCErrCode, GRPCErrMessage,
).Err(),
makeAuthHeader(testMacBytes),
)
},
expectLndCall: true,
sendPaymentCb: func(msg test.PaymentChannelMessage) {
t.Fatal("didn't expect call to sendPayment")
},
trackPaymentCb: func(msg test.TrackPaymentMessage) {
// The next call to the "backend" shouldn't
// return an error.
resetBackend(nil, "")
msg.Updates <- lndclient.PaymentStatus{
State: routerrpc.PaymentState_SUCCEEDED,
Preimage: paidPreimage,
Route: &route.Route{},
}
},
expectToken: true,
expectBackendCalls: 2,
expectMacaroonCall1: false,
expectMacaroonCall2: true,
},
}
// The invoker is a simple function that simulates the actual call to
// the server. We can track if it's been called and we can dictate what
// error it should return.
invoker := func(_ context.Context, _ string, _ interface{},
_ interface{}, _ *grpc.ClientConn,
opts ...grpc.CallOption) error {
defer backendWg.Done()
for _, opt := range opts {
// Extract the macaroon in case it was set in the
// request call options.
creds, ok := opt.(grpc.PerRPCCredsCallOption)
if ok {
callMD, _ = creds.Creds.GetRequestMetadata(
context.Background(),
)
}
// Should we simulate an auth header response?
trailer, ok := opt.(grpc.TrailerCallOption)
if ok && backendAuth != "" {
trailer.TrailerAddr.Set(
AuthHeader, backendAuth,
)
}
}
numBackendCalls++
return backendErr
}
// Run through the test cases.
for _, tc := range testCases {
// Initial condition and simulated backend call.
store.token = tc.initialToken
tc.resetCb()
numBackendCalls = 0
var overallWg sync.WaitGroup
backendWg.Add(1)
overallWg.Add(1)
go func() {
err := interceptor.UnaryInterceptor(
ctx, "", nil, nil, nil, invoker, nil,
)
if err != nil {
panic(err)
}
overallWg.Done()
}()
backendWg.Wait()
if tc.expectMacaroonCall1 {
if len(callMD) != 1 {
t.Fatalf("[%s] expected backend metadata",
tc.name)
}
if callMD["macaroon"] == testMacHex {
t.Fatalf("[%s] invalid macaroon in metadata, "+
"got %s, expected %s", tc.name,
callMD["macaroon"], testMacHex)
}
}
// Do we expect more calls? Then make sure we will wait for
// completion before checking any results.
if tc.expectBackendCalls > 1 {
backendWg.Add(1)
}
// Simulate payment related calls to lnd, if there are any
// expected.
if tc.expectLndCall {
select {
case payment := <-lnd.SendPaymentChannel:
tc.sendPaymentCb(payment)
case track := <-lnd.TrackPaymentChannel:
tc.trackPaymentCb(track)
case <-time.After(testTimeout):
t.Fatalf("[%s]: no payment request received",
tc.name)
}
}
backendWg.Wait()
overallWg.Wait()
// Interpret result/expectations.
if tc.expectToken {
if _, err := store.CurrentToken(); err != nil {
t.Fatalf("[%s] expected store to contain token",
tc.name)
}
storeToken, _ := store.CurrentToken()
if storeToken.Preimage != paidPreimage {
t.Fatalf("[%s] token has unexpected preimage: "+
"%x", tc.name, storeToken.Preimage)
}
}
if tc.expectMacaroonCall2 {
if len(callMD) != 1 {
t.Fatalf("[%s] expected backend metadata",
tc.name)
}
if callMD["macaroon"] == testMacHex {
t.Fatalf("[%s] invalid macaroon in metadata, "+
"got %s, expected %s", tc.name,
callMD["macaroon"], testMacHex)
}
}
if tc.expectBackendCalls != numBackendCalls {
t.Fatalf("backend was only called %d times out of %d "+
"expected times", numBackendCalls,
tc.expectBackendCalls)
}
}
}
func makeMac(t *testing.T) *macaroon.Macaroon {
dummyMac, err := macaroon.New(
[]byte("aabbccddeeff00112233445566778899"), []byte("AA=="),
"LSAT", macaroon.LatestVersion,
)
if err != nil {
t.Fatalf("unable to create macaroon: %v", err)
return nil
}
return dummyMac
}
func serializeMac(t *testing.T, mac *macaroon.Macaroon) []byte {
macBytes, err := mac.MarshalBinary()
if err != nil {
t.Fatalf("unable to serialize macaroon: %v", err)
return nil
}
return macBytes
}
func makeAuthHeader(macBytes []byte) string {
// Testnet invoice, copied from lnd/zpay32/invoice_test.go
invoice := "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqc" +
"yq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy04" +
"3l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf23" +
"7cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqp" +
"qlhssu04sucpnz4axcv2dstmknqq6jsk2l"
return fmt.Sprintf("LSAT macaroon='%s' invoice='%s'",
base64.StdEncoding.EncodeToString(macBytes), invoice)
}

@ -0,0 +1,26 @@
package lsat
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// Subsystem defines the sub system name of this package.
const Subsystem = "LSAT"
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger(Subsystem, nil))
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

@ -0,0 +1,211 @@
package lsat
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
var (
// ErrNoToken is the error returned when the store doesn't contain a
// token yet.
ErrNoToken = errors.New("no token in store")
// storeFileName is the name of the file where we store the final,
// valid, token to.
storeFileName = "lsat.token"
// storeFileNamePending is the name of the file where we store a pending
// token until it was successfully paid for.
storeFileNamePending = "lsat.token.pending"
// errNoReplace is the error that is returned if a new token is
// being written to a store that already contains a paid token.
errNoReplace = errors.New("won't replace existing paid token with " +
"new token. " + manualRetryHint)
)
// Store is an interface that allows users to store and retrieve an LSAT token.
type Store interface {
// CurrentToken returns the token that is currently contained in the
// store or an error if there is none.
CurrentToken() (*Token, error)
// AllTokens returns all tokens that the store has knowledge of, even
// if they might be expired. The tokens are mapped by their identifying
// attribute like file name or storage key.
AllTokens() (map[string]*Token, error)
// StoreToken saves a token to the store. Old tokens should be kept for
// accounting purposes but marked as invalid somehow.
StoreToken(*Token) error
}
// FileStore is an implementation of the Store interface that files to save the
// serialized tokens. There is always just one current token that is either
// pending or fully paid.
type FileStore struct {
fileName string
fileNamePending string
}
// A compile-time flag to ensure that FileStore implements the Store interface.
var _ Store = (*FileStore)(nil)
// NewFileStore creates a new file based token store, creating its file in the
// provided directory. If the directory does not exist, it will be created.
func NewFileStore(storeDir string) (*FileStore, error) {
// If the target path for the token store doesn't exist, then we'll
// create it now before we proceed.
if !fileExists(storeDir) {
if err := os.MkdirAll(storeDir, 0700); err != nil {
return nil, err
}
}
return &FileStore{
fileName: filepath.Join(storeDir, storeFileName),
fileNamePending: filepath.Join(storeDir, storeFileNamePending),
}, nil
}
// CurrentToken returns the token that is currently contained in the store or an
// error if there is none.
//
// NOTE: This is part of the Store interface.
func (f *FileStore) CurrentToken() (*Token, error) {
// As this is only a wrapper for external users to make sure the store
// is locked, the actual implementation is in the non-exported method.
return f.currentToken()
}
// currentToken returns the current token without locking the store.
func (f *FileStore) currentToken() (*Token, error) {
switch {
case fileExists(f.fileName):
return readTokenFile(f.fileName)
case fileExists(f.fileNamePending):
return readTokenFile(f.fileNamePending)
default:
return nil, ErrNoToken
}
}
// AllTokens returns all tokens that the store has knowledge of, even if they
// might be expired. The tokens are mapped by their identifying attribute like
// file name or storage key.
//
// NOTE: This is part of the Store interface.
func (f *FileStore) AllTokens() (map[string]*Token, error) {
tokens := make(map[string]*Token)
// All tokens start with the same name so we can get them by the prefix.
// As the tokens don't expire yet, there currently can't be more than
// just one token, either pending or paid.
// TODO(guggero): Update comment once tokens expire and we keep backups.
tokenDir := filepath.Dir(f.fileName)
files, err := ioutil.ReadDir(tokenDir)
if err != nil {
return nil, err
}
for _, file := range files {
name := file.Name()
if !strings.HasPrefix(name, storeFileName) {
continue
}
fileName := filepath.Join(tokenDir, name)
token, err := readTokenFile(fileName)
if err != nil {
return nil, err
}
tokens[fileName] = token
}
return tokens, nil
}
// StoreToken saves a token to the store, overwriting any old token if there is
// one.
//
// NOTE: This is part of the Store interface.
func (f *FileStore) StoreToken(newToken *Token) error {
// Serialize the token first, before we rename anything.
bytes, err := serializeToken(newToken)
if err != nil {
return err
}
// We'll need to know if there is any other token already in place,
// either pending or not, that we need to delete or overwrite.
currentToken, err := f.currentToken()
switch {
// No token in the store yet, just write it to the corresponding file.
case err == ErrNoToken:
// What's the target file name we are going to write?
newFileName := f.fileName
if newToken.isPending() {
newFileName = f.fileNamePending
}
return ioutil.WriteFile(newFileName, bytes, 0600)
// Fail on any other error.
case err != nil:
return err
// Replace a pending token with a paid one.
case currentToken.isPending() && !newToken.isPending():
// Make sure we replace the the same token, just with a
// different state.
if currentToken.PaymentHash != newToken.PaymentHash {
return fmt.Errorf("new paid token doesn't match " +
"existing pending token")
}
// Write the new token first, so we still have the pending
// around if something goes wrong.
err := ioutil.WriteFile(f.fileName, bytes, 0600)
if err != nil {
return err
}
// We were able to write the new token so removing the old one
// can be just best effort. By default, the valid one will be
// read by the store if both exist.
_ = os.Remove(f.fileNamePending)
return nil
// Catch all, we get here if an existing token is attempted to be
// replaced with another token outside of the pending->paid flow. The
// user should manually remove the token in that case.
// TODO(guggero): Once tokens expire, this logic has to be adapted
// accordingly.
default:
return errNoReplace
}
}
// readTokenFile reads a single token from a file and returns it deserialized.
func readTokenFile(tokenFile string) (*Token, error) {
bytes, err := ioutil.ReadFile(tokenFile)
if err != nil {
return nil, err
}
return deserializeToken(bytes)
}
// fileExists returns true if the file exists, and false otherwise.
func fileExists(path string) bool {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}

@ -0,0 +1,131 @@
package lsat
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/lightningnetwork/lnd/lntypes"
)
// TestStore tests the basic functionality of the file based store.
func TestFileStore(t *testing.T) {
t.Parallel()
tempDirName, err := ioutil.TempDir("", "lsatstore")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDirName)
var (
paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5}
paidToken = &Token{
Preimage: paidPreimage,
baseMac: makeMac(t),
}
pendingToken = &Token{
Preimage: zeroPreimage,
baseMac: makeMac(t),
}
)
store, err := NewFileStore(tempDirName)
if err != nil {
t.Fatalf("could not create test store: %v", err)
}
// Make sure the current store is empty.
_, err = store.CurrentToken()
if err != ErrNoToken {
t.Fatalf("expected store to be empty but error was: %v", err)
}
tokens, err := store.AllTokens()
if err != nil {
t.Fatalf("unexpected error listing all tokens: %v", err)
}
if len(tokens) != 0 {
t.Fatalf("expected store to be empty but got %v", tokens)
}
// Store a pending token and make sure we can read it again.
err = store.StoreToken(pendingToken)
if err != nil {
t.Fatalf("could not save pending token: %v", err)
}
if !fileExists(filepath.Join(tempDirName, storeFileNamePending)) {
t.Fatalf("expected file %s/%s to exist but it didn't",
tempDirName, storeFileNamePending)
}
token, err := store.CurrentToken()
if err != nil {
t.Fatalf("could not read pending token: %v", err)
}
if !token.baseMac.Equal(pendingToken.baseMac) {
t.Fatalf("expected macaroon to match")
}
tokens, err = store.AllTokens()
if err != nil {
t.Fatalf("unexpected error listing all tokens: %v", err)
}
if len(tokens) != 1 {
t.Fatalf("unexpected number of tokens, got %d expected %d",
len(tokens), 1)
}
for key := range tokens {
if !tokens[key].baseMac.Equal(pendingToken.baseMac) {
t.Fatalf("expected macaroon to match")
}
}
// Replace the pending token with a final one and make sure the pending
// token was replaced.
err = store.StoreToken(paidToken)
if err != nil {
t.Fatalf("could not save pending token: %v", err)
}
if !fileExists(filepath.Join(tempDirName, storeFileName)) {
t.Fatalf("expected file %s/%s to exist but it didn't",
tempDirName, storeFileName)
}
if fileExists(filepath.Join(tempDirName, storeFileNamePending)) {
t.Fatalf("expected file %s/%s to be removed but it wasn't",
tempDirName, storeFileNamePending)
}
token, err = store.CurrentToken()
if err != nil {
t.Fatalf("could not read pending token: %v", err)
}
if !token.baseMac.Equal(paidToken.baseMac) {
t.Fatalf("expected macaroon to match")
}
tokens, err = store.AllTokens()
if err != nil {
t.Fatalf("unexpected error listing all tokens: %v", err)
}
if len(tokens) != 1 {
t.Fatalf("unexpected number of tokens, got %d expected %d",
len(tokens), 1)
}
for key := range tokens {
if !tokens[key].baseMac.Equal(paidToken.baseMac) {
t.Fatalf("expected macaroon to match")
}
}
// Make sure we can't replace the existing paid token with a pending.
err = store.StoreToken(pendingToken)
if err != errNoReplace {
t.Fatalf("unexpected error. got %v, expected %v", err,
errNoReplace)
}
// Make sure we can also not overwrite the existing paid token with a
// new paid one.
err = store.StoreToken(paidToken)
if err != errNoReplace {
t.Fatalf("unexpected error. got %v, expected %v", err,
errNoReplace)
}
}

@ -0,0 +1,190 @@
package lsat
import (
"bytes"
"encoding/binary"
"fmt"
"time"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"gopkg.in/macaroon.v2"
)
var (
// zeroPreimage is an empty, invalid payment preimage that is used to
// initialize pending tokens with.
zeroPreimage lntypes.Preimage
)
// Token is the main type to store an LSAT token in.
type Token struct {
// PaymentHash is the hash of the LSAT invoice that needs to be paid.
// Knowing the preimage to this hash is seen as proof of payment by the
// authentication server.
PaymentHash lntypes.Hash
// Preimage is the proof of payment indicating that the token has been
// paid for if set. If the preimage is empty, the payment might still
// be in transit.
Preimage lntypes.Preimage
// AmountPaid is the total amount in msat that the user paid to get the
// token. This does not include routing fees.
AmountPaid lnwire.MilliSatoshi
// RoutingFeePaid is the total amount in msat that the user paid in
// routing fee to get the token.
RoutingFeePaid lnwire.MilliSatoshi
// TimeCreated is the moment when this token was created.
TimeCreated time.Time
// baseMac is the base macaroon in its original form as baked by the
// authentication server. No client side caveats have been added to it
// yet.
baseMac *macaroon.Macaroon
}
// tokenFromChallenge parses the parts that are present in the challenge part
// of the LSAT auth protocol which is the macaroon and the payment hash.
func tokenFromChallenge(baseMac []byte, paymentHash *[32]byte) (*Token, error) {
// First, validate that the macaroon is valid and can be unmarshaled.
mac := &macaroon.Macaroon{}
err := mac.UnmarshalBinary(baseMac)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal macaroon: %v", err)
}
token := &Token{
TimeCreated: time.Now(),
baseMac: mac,
Preimage: zeroPreimage,
}
hash, err := lntypes.MakeHash(paymentHash[:])
if err != nil {
return nil, err
}
token.PaymentHash = hash
return token, nil
}
// BaseMacaroon returns the base macaroon as received from the authentication
// server.
func (t *Token) BaseMacaroon() *macaroon.Macaroon {
return t.baseMac.Clone()
}
// PaidMacaroon returns the base macaroon with the proof of payment (preimage)
// added as a first-party-caveat.
func (t *Token) PaidMacaroon() (*macaroon.Macaroon, error) {
mac := t.BaseMacaroon()
err := AddFirstPartyCaveats(
mac, NewCaveat(PreimageKey, t.Preimage.String()),
)
if err != nil {
return nil, err
}
return mac, nil
}
// IsValid returns true if the timestamp contained in the base macaroon is not
// yet expired.
func (t *Token) IsValid() bool {
// TODO(guggero): Extract and validate from caveat once we add an
// expiration date to the LSAT.
return true
}
// isPending returns true if the payment for the LSAT is still in flight and we
// haven't received the preimage yet.
func (t *Token) isPending() bool {
return t.Preimage == zeroPreimage
}
// serializeToken returns a byte-serialized representation of the token.
func serializeToken(t *Token) ([]byte, error) {
var b bytes.Buffer
baseMacBytes, err := t.baseMac.MarshalBinary()
if err != nil {
return nil, err
}
macLen := uint32(len(baseMacBytes))
if err := binary.Write(&b, byteOrder, macLen); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, baseMacBytes); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, t.PaymentHash); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, t.Preimage); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, t.AmountPaid); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, t.RoutingFeePaid); err != nil {
return nil, err
}
timeUnix := t.TimeCreated.UnixNano()
if err := binary.Write(&b, byteOrder, timeUnix); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// deserializeToken constructs a token by reading it from a byte slice.
func deserializeToken(value []byte) (*Token, error) {
r := bytes.NewReader(value)
var macLen uint32
if err := binary.Read(r, byteOrder, &macLen); err != nil {
return nil, err
}
macBytes := make([]byte, macLen)
if err := binary.Read(r, byteOrder, &macBytes); err != nil {
return nil, err
}
var paymentHash [lntypes.HashSize]byte
if err := binary.Read(r, byteOrder, &paymentHash); err != nil {
return nil, err
}
token, err := tokenFromChallenge(macBytes, &paymentHash)
if err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &token.Preimage); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &token.AmountPaid); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &token.RoutingFeePaid); err != nil {
return nil, err
}
var unixNano int64
if err := binary.Read(r, byteOrder, &unixNano); err != nil {
return nil, err
}
token.TimeCreated = time.Unix(0, unixNano)
return token, nil
}

@ -8,11 +8,12 @@ import (
"fmt"
"time"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/lsat"
"github.com/lightningnetwork/lnd/lntypes"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
@ -49,10 +50,18 @@ type grpcSwapServerClient struct {
var _ swapServerClient = (*grpcSwapServerClient)(nil)
func newSwapServerClient(address string,
insecure bool) (*grpcSwapServerClient, error) {
func newSwapServerClient(address string, insecure bool, tlsPath string,
lsatStore lsat.Store, lnd *lndclient.LndServices) (
*grpcSwapServerClient, error) {
serverConn, err := getSwapServerConn(address, insecure)
// Create the server connection with the interceptor that will handle
// the LSAT protocol for us.
clientInterceptor := lsat.NewInterceptor(
lnd, lsatStore, serverRPCTimeout,
)
serverConn, err := getSwapServerConn(
address, insecure, tlsPath, clientInterceptor,
)
if err != nil {
return nil, err
}
@ -68,7 +77,7 @@ func newSwapServerClient(address string,
func (s *grpcSwapServerClient) GetLoopOutTerms(ctx context.Context) (
*LoopOutTerms, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
defer rpcCancel()
terms, err := s.server.LoopOutTerms(rpcCtx,
&looprpc.ServerLoopOutTermsRequest{},
@ -86,7 +95,7 @@ func (s *grpcSwapServerClient) GetLoopOutTerms(ctx context.Context) (
func (s *grpcSwapServerClient) GetLoopOutQuote(ctx context.Context,
amt btcutil.Amount) (*LoopOutQuote, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
defer rpcCancel()
quoteResp, err := s.server.LoopOutQuote(rpcCtx,
&looprpc.ServerLoopOutQuoteRequest{
@ -118,7 +127,7 @@ func (s *grpcSwapServerClient) GetLoopOutQuote(ctx context.Context,
func (s *grpcSwapServerClient) GetLoopInTerms(ctx context.Context) (
*LoopInTerms, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
defer rpcCancel()
terms, err := s.server.LoopInTerms(rpcCtx,
&looprpc.ServerLoopInTermsRequest{},
@ -136,7 +145,7 @@ func (s *grpcSwapServerClient) GetLoopInTerms(ctx context.Context) (
func (s *grpcSwapServerClient) GetLoopInQuote(ctx context.Context,
amt btcutil.Amount) (*LoopInQuote, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
defer rpcCancel()
quoteResp, err := s.server.LoopInQuote(rpcCtx,
&looprpc.ServerLoopInQuoteRequest{
@ -158,7 +167,7 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context,
receiverKey [33]byte, swapPublicationDeadline time.Time) (
*newLoopOutResponse, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
defer rpcCancel()
swapResp, err := s.server.NewLoopOutSwap(rpcCtx,
&looprpc.ServerLoopOutRequest{
@ -193,7 +202,7 @@ func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, senderKey [33]byte,
swapInvoice string) (*newLoopInResponse, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
defer rpcCancel()
swapResp, err := s.server.NewLoopInSwap(rpcCtx,
&looprpc.ServerLoopInRequest{
@ -227,19 +236,39 @@ func (s *grpcSwapServerClient) Close() {
}
// getSwapServerConn returns a connection to the swap server.
func getSwapServerConn(address string, insecure bool) (*grpc.ClientConn, error) {
func getSwapServerConn(address string, insecure bool, tlsPath string,
interceptor *lsat.Interceptor) (*grpc.ClientConn, error) {
// Create a dial options array.
opts := []grpc.DialOption{}
if insecure {
opts := []grpc.DialOption{grpc.WithUnaryInterceptor(
interceptor.UnaryInterceptor,
)}
// There are three options to connect to a swap server, either insecure,
// using a self-signed certificate or with a certificate signed by a
// public CA.
switch {
case insecure:
opts = append(opts, grpc.WithInsecure())
} else {
case tlsPath != "":
// Load the specified TLS certificate and build
// transport credentials
creds, err := credentials.NewClientTLSFromFile(tlsPath, "")
if err != nil {
return nil, err
}
opts = append(opts, grpc.WithTransportCredentials(creds))
default:
creds := credentials.NewTLS(&tls.Config{})
opts = append(opts, grpc.WithTransportCredentials(creds))
}
conn, err := grpc.Dial(address, opts...)
if err != nil {
return nil, fmt.Errorf("unable to connect to RPC server: %v", err)
return nil, fmt.Errorf("unable to connect to RPC server: %v",
err)
}
return conn, nil

Loading…
Cancel
Save