diff --git a/rfq/manager.go b/rfq/manager.go index f93dcd5fb..7eac03c60 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -4,19 +4,26 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "fmt" + "sort" "sync" "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" lfn "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/zpay32" ) const ( @@ -232,6 +239,7 @@ func (m *Manager) startSubsystems(ctx context.Context) error { HtlcInterceptor: m.cfg.HtlcInterceptor, HtlcSubscriber: m.cfg.HtlcSubscriber, AcceptHtlcEvents: m.acceptHtlcEvents, + SpecifierChecker: m.AssetMatchesSpecifier, }) if err != nil { return fmt.Errorf("error initializing RFQ order handler: %w", @@ -948,6 +956,10 @@ func (m *Manager) getAssetGroupKey(ctx context.Context, // Perform the DB query. group, err := m.cfg.GroupLookup.QueryAssetGroup(ctx, id) if err != nil { + if errors.Is(err, address.ErrAssetGroupUnknown) { + return fn.None[btcec.PublicKey](), nil + } + return fn.None[btcec.PublicKey](), err } @@ -971,6 +983,18 @@ func (m *Manager) AssetMatchesSpecifier(ctx context.Context, switch { case specifier.HasGroupPubKey(): + specifierGK := specifier.UnwrapGroupKeyToPtr() + + // Let's directly check if the ID is equal to the hash of the + // group key. This is used by the sender to indicate that any + // asset that belongs to this group may be used. + groupKeyX := schnorr.SerializePubKey(specifierGK) + if asset.ID(groupKeyX) == id { + return true, nil + } + + // Now let's make an actual query to find this assetID's group, + // if it exists. group, err := m.getAssetGroupKey(ctx, id) if err != nil { return false, err @@ -980,8 +1004,6 @@ func (m *Manager) AssetMatchesSpecifier(ctx context.Context, return false, nil } - specifierGK := specifier.UnwrapGroupKeyToPtr() - return group.UnwrapToPtr().IsEqual(specifierGK), nil case specifier.HasId(): @@ -1028,6 +1050,226 @@ func (m *Manager) ChannelCompatible(ctx context.Context, return true, nil } +// ChannelWithSpecifier is a helper struct that combines the information of an +// asset specifier that is satisfied by a channel with the channels' general +// information. +type ChannelWithSpecifier struct { + // Specifier is the asset Specifier that is satisfied by this channels' + // assets. + Specifier asset.Specifier + + // ChannelInfo is the information about the channel the asset is + // committed to. + ChannelInfo lndclient.ChannelInfo + + // AssetInfo contains the asset related info of the channel. + AssetInfo rfqmsg.JsonAssetChanInfo +} + +// ComputeChannelAssetBalance computes the total local and remote balance for +// each asset channel that matches the provided asset specifier. +func (m *Manager) ComputeChannelAssetBalance(ctx context.Context, + activeChannels []lndclient.ChannelInfo, + specifier asset.Specifier) (map[route.Vertex][]ChannelWithSpecifier, + error) { + + peerChanMap := make(map[route.Vertex][]ChannelWithSpecifier) + + for chanIdx := range activeChannels { + openChan := activeChannels[chanIdx] + if len(openChan.CustomChannelData) == 0 { + continue + } + + var assetData rfqmsg.JsonAssetChannel + err := json.Unmarshal(openChan.CustomChannelData, &assetData) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal asset "+ + "data: %w", err) + } + + // Check if the assets of this channel match the provided + // specifier. + pass, err := m.ChannelCompatible( + ctx, assetData.Assets, specifier, + ) + if err != nil { + return nil, err + } + + if pass { + // Since the assets of the channel passed the above + // filter, we're safe to aggregate their info to be + // represented as a single entity. + var aggrInfo rfqmsg.JsonAssetChanInfo + + // TODO(george): refactor when JSON gets fixed + for _, info := range assetData.Assets { + aggrInfo.Capacity += info.Capacity + aggrInfo.LocalBalance += info.LocalBalance + aggrInfo.RemoteBalance += info.RemoteBalance + } + + _, ok := peerChanMap[openChan.PubKeyBytes] + if !ok { + peerChanMap[openChan.PubKeyBytes] = + make([]ChannelWithSpecifier, 0) + } + + chanMap := peerChanMap[openChan.PubKeyBytes] + + chanMap = append(chanMap, ChannelWithSpecifier{ + Specifier: specifier, + ChannelInfo: openChan, + AssetInfo: aggrInfo, + }) + + peerChanMap[openChan.PubKeyBytes] = chanMap + } + } + + return peerChanMap, nil +} + +// ChanLister is a helper that is able to list the channels of the node. +type ChanLister func(ctx context.Context, activeOnly, + publicOnly bool) ([]lndclient.ChannelInfo, error) + +// chanIntention defines the intention of calling rfqChannel. This helps with +// returning the channel that is most suitable for what we want to do. +type ChanIntention uint8 + +const ( + // NoIntention defines the absence of any intention, signalling that we + // don't really care which channel is returned. + NoIntention ChanIntention = iota + + // SendIntention defines the intention to send over an asset channel. + SendIntention + + // ReceiveIntention defines the intention to receive over an asset + // channel. + ReceiveIntention +) + +// RfqChannel returns the channel to use for RFQ operations. It returns a map of +// peers and their eligible channels. If a peerPubKey is specified then the map +// will only contain one entry for that peer. +func (m *Manager) RfqChannel(ctx context.Context, + chanLister ChanLister, specifier asset.Specifier, + peerPubKey *route.Vertex, intention ChanIntention) (map[route.Vertex][]ChannelWithSpecifier, + error) { + + activeChannels, err := chanLister(ctx, true, false) + if err != nil { + return nil, err + } + + balancesMap, err := m.ComputeChannelAssetBalance( + ctx, activeChannels, specifier, + ) + if err != nil { + return nil, fmt.Errorf("error computing available asset "+ + "channel balance: %w", err) + } + + if len(balancesMap) == 0 { + return nil, fmt.Errorf("no asset channel balance found for %s", + &specifier) + } + + switch intention { + case SendIntention: + // When sending we care about the volume of our local balances, + // so we sort by local balances in descending order. + for _, v := range balancesMap { + sort.Slice(v, func(i, j int) bool { + return v[i].AssetInfo.LocalBalance > + v[j].AssetInfo.LocalBalance + }) + } + case ReceiveIntention: + // When sending we care about the volume of the remote balances, + // so we sort by remote balances in descending order. + for _, v := range balancesMap { + sort.Slice(v, func(i, j int) bool { + return v[i].AssetInfo.RemoteBalance > + v[j].AssetInfo.RemoteBalance + }) + } + case NoIntention: + // We don't care about sending or receiving, this means that + // the method was called as a dry check. Do nothing. + } + + // If a peer public key was specified, we always want to use that to + // filter the asset channels. + if peerPubKey != nil { + _, ok := balancesMap[*peerPubKey] + if !ok { + return nil, fmt.Errorf("no asset channels found for "+ + "%s and peer=%s", &specifier, peerPubKey) + } + } + + return balancesMap, nil +} + +// InboundPolicyFetcher is a helper that fetches the inbound policy of a channel +// based on its chanID. +type InboundPolicyFetcher func(ctx context.Context, chanID uint64, + remotePubStr string) (*lnrpc.RoutingPolicy, error) + +// RfqToHopHint creates the hop hint representation which encapsulates certain +// quote information along with some other data required by the payment to +// succeed. +func (m *Manager) RfqToHopHint(ctx context.Context, + policyFetcher InboundPolicyFetcher, channelID uint64, + peerPubKey route.Vertex, quote *rfqrpc.PeerAcceptedBuyQuote, + hold bool) (*lnrpc.HopHint, []zpay32.HopHint, error) { + + inboundPolicy, err := policyFetcher(ctx, channelID, peerPubKey.String()) + if err != nil { + return nil, nil, fmt.Errorf("unable to get inbound channel "+ + "policy for channel with ID %d: %w", channelID, err) + } + + if hold { + peerPub, err := btcec.ParsePubKey(peerPubKey[:]) + if err != nil { + return nil, nil, fmt.Errorf("error parsing peer "+ + "pubkey: %w", err) + } + hopHint := []zpay32.HopHint{ + { + NodeID: peerPub, + ChannelID: quote.Scid, + FeeBaseMSat: uint32(inboundPolicy.FeeBaseMsat), + FeeProportionalMillionths: uint32( + inboundPolicy.FeeRateMilliMsat, + ), + CLTVExpiryDelta: uint16( + inboundPolicy.TimeLockDelta, + ), + }, + } + + return nil, hopHint, nil + } + + hopHint := &lnrpc.HopHint{ + NodeId: peerPubKey.String(), + ChanId: quote.Scid, + FeeBaseMsat: uint32(inboundPolicy.FeeBaseMsat), + FeeProportionalMillionths: uint32( + inboundPolicy.FeeRateMilliMsat, + ), + CltvExpiryDelta: inboundPolicy.TimeLockDelta, + } + + return hopHint, nil, nil +} + // publishSubscriberEvent publishes an event to all subscribers. func (m *Manager) publishSubscriberEvent(event fn.Event) { // Iterate over the subscribers and deliver the event to each one. diff --git a/rfq/marshal.go b/rfq/marshal.go new file mode 100644 index 000000000..6c8cd1030 --- /dev/null +++ b/rfq/marshal.go @@ -0,0 +1,173 @@ +package rfq + +import ( + "fmt" + + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" +) + +// MarshalAcceptedSellQuoteEvent marshals a peer accepted sell quote event to +// its RPC representation. +func MarshalAcceptedSellQuoteEvent( + event *PeerAcceptedSellQuoteEvent) *rfqrpc.PeerAcceptedSellQuote { + + return MarshalAcceptedSellQuote(event.SellAccept) +} + +// MarshalAcceptedSellQuote marshals a peer accepted sell quote to its RPC +// representation. +func MarshalAcceptedSellQuote( + accept rfqmsg.SellAccept) *rfqrpc.PeerAcceptedSellQuote { + + rpcAssetRate := &rfqrpc.FixedPoint{ + Coefficient: accept.AssetRate.Rate.Coefficient.String(), + Scale: uint32(accept.AssetRate.Rate.Scale), + } + + // Calculate the equivalent asset units for the given total BTC amount + // based on the asset-to-BTC conversion rate. + numAssetUnits := rfqmath.MilliSatoshiToUnits( + accept.Request.PaymentMaxAmt, accept.AssetRate.Rate, + ) + + minTransportableMSat := rfqmath.MinTransportableMSat( + rfqmath.DefaultOnChainHtlcMSat, accept.AssetRate.Rate, + ) + + return &rfqrpc.PeerAcceptedSellQuote{ + Peer: accept.Peer.String(), + Id: accept.ID[:], + Scid: uint64(accept.ShortChannelId()), + BidAssetRate: rpcAssetRate, + Expiry: uint64(accept.AssetRate.Expiry.Unix()), + AssetAmount: numAssetUnits.ScaleTo(0).ToUint64(), + MinTransportableMsat: uint64(minTransportableMSat), + } +} + +// MarshalAcceptedBuyQuoteEvent marshals a peer accepted buy quote event to +// its rpc representation. +func MarshalAcceptedBuyQuoteEvent( + event *PeerAcceptedBuyQuoteEvent) (*rfqrpc.PeerAcceptedBuyQuote, + error) { + + // We now calculate the minimum amount of asset units that can be + // transported within a single HTLC for this asset at the given rate. + // This corresponds to the 354 satoshi minimum non-dust HTLC value. + minTransportableUnits := rfqmath.MinTransportableUnits( + rfqmath.DefaultOnChainHtlcMSat, event.AssetRate.Rate, + ).ScaleTo(0).ToUint64() + + return &rfqrpc.PeerAcceptedBuyQuote{ + Peer: event.Peer.String(), + Id: event.ID[:], + Scid: uint64(event.ShortChannelId()), + AssetMaxAmount: event.Request.AssetMaxAmt, + AskAssetRate: &rfqrpc.FixedPoint{ + Coefficient: event.AssetRate.Rate.Coefficient.String(), + Scale: uint32(event.AssetRate.Rate.Scale), + }, + Expiry: uint64(event.AssetRate.Expiry.Unix()), + MinTransportableUnits: minTransportableUnits, + }, nil +} + +// MarshalInvalidQuoteRespEvent marshals an invalid quote response event to +// its rpc representation. +func MarshalInvalidQuoteRespEvent( + event *InvalidQuoteRespEvent) *rfqrpc.InvalidQuoteResponse { + + peer := event.QuoteResponse.MsgPeer() + id := event.QuoteResponse.MsgID() + + return &rfqrpc.InvalidQuoteResponse{ + Status: rfqrpc.QuoteRespStatus(event.Status), + Peer: peer.String(), + Id: id[:], + } +} + +// MarshalIncomingRejectQuoteEvent marshals an incoming reject quote event to +// its RPC representation. +func MarshalIncomingRejectQuoteEvent( + event *IncomingRejectQuoteEvent) *rfqrpc.RejectedQuoteResponse { + + return &rfqrpc.RejectedQuoteResponse{ + Peer: event.Peer.String(), + Id: event.ID.Val[:], + ErrorMessage: event.Err.Val.Msg, + ErrorCode: uint32(event.Err.Val.Code), + } +} + +// NewAddAssetBuyOrderResponse creates a new AddAssetBuyOrderResponse from +// the given RFQ event. +func NewAddAssetBuyOrderResponse( + event fn.Event) (*rfqrpc.AddAssetBuyOrderResponse, error) { + + resp := &rfqrpc.AddAssetBuyOrderResponse{} + + switch e := event.(type) { + case *PeerAcceptedBuyQuoteEvent: + acceptedQuote, err := MarshalAcceptedBuyQuoteEvent(e) + if err != nil { + return nil, err + } + + resp.Response = &rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote{ + AcceptedQuote: acceptedQuote, + } + return resp, nil + + case *InvalidQuoteRespEvent: + resp.Response = &rfqrpc.AddAssetBuyOrderResponse_InvalidQuote{ + InvalidQuote: MarshalInvalidQuoteRespEvent(e), + } + return resp, nil + + case *IncomingRejectQuoteEvent: + resp.Response = &rfqrpc.AddAssetBuyOrderResponse_RejectedQuote{ + RejectedQuote: MarshalIncomingRejectQuoteEvent(e), + } + return resp, nil + + default: + return nil, fmt.Errorf("unknown AddAssetBuyOrder event "+ + "type: %T", e) + } +} + +// NewAddAssetSellOrderResponse creates a new AddAssetSellOrderResponse from +// the given RFQ event. +func NewAddAssetSellOrderResponse( + event fn.Event) (*rfqrpc.AddAssetSellOrderResponse, error) { + + resp := &rfqrpc.AddAssetSellOrderResponse{} + + switch e := event.(type) { + case *PeerAcceptedSellQuoteEvent: + resp.Response = &rfqrpc.AddAssetSellOrderResponse_AcceptedQuote{ + AcceptedQuote: MarshalAcceptedSellQuoteEvent(e), + } + return resp, nil + + case *InvalidQuoteRespEvent: + resp.Response = &rfqrpc.AddAssetSellOrderResponse_InvalidQuote{ + InvalidQuote: MarshalInvalidQuoteRespEvent(e), + } + return resp, nil + + case *IncomingRejectQuoteEvent: + resp.Response = &rfqrpc.AddAssetSellOrderResponse_RejectedQuote{ + RejectedQuote: MarshalIncomingRejectQuoteEvent(e), + } + return resp, nil + + default: + return nil, fmt.Errorf("unknown AddAssetSellOrder event "+ + "type: %T", e) + } +} diff --git a/rfq/order.go b/rfq/order.go index 3dcf1a331..2b5c58f15 100644 --- a/rfq/order.go +++ b/rfq/order.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/asset" @@ -58,7 +59,8 @@ type SerialisedScid = rfqmsg.SerialisedScid type Policy interface { // CheckHtlcCompliance returns an error if the given HTLC intercept // descriptor does not satisfy the subject policy. - CheckHtlcCompliance(htlc lndclient.InterceptedHtlc) error + CheckHtlcCompliance(ctx context.Context, htlc lndclient.InterceptedHtlc, + specifierChecker rfqmsg.SpecifierChecker) error // Expiry returns the policy's expiry time as a unix timestamp. Expiry() uint64 @@ -145,8 +147,8 @@ func NewAssetSalePolicy(quote rfqmsg.BuyAccept) *AssetSalePolicy { // included as a hop hint within the invoice. The SCID is the only piece of // information used to determine the policy applicable to the HTLC. As a result, // HTLC custom records are not expected to be present. -func (c *AssetSalePolicy) CheckHtlcCompliance( - htlc lndclient.InterceptedHtlc) error { +func (c *AssetSalePolicy) CheckHtlcCompliance(_ context.Context, + htlc lndclient.InterceptedHtlc, _ rfqmsg.SpecifierChecker) error { // Since we will be reading CurrentAmountMsat value we acquire a read // lock. @@ -248,11 +250,23 @@ func (c *AssetSalePolicy) GenerateInterceptorResponse( outgoingAmt := rfqmath.DefaultOnChainHtlcMSat - // Unpack asset ID. - assetID, err := c.AssetSpecifier.UnwrapIdOrErr() - if err != nil { - return nil, fmt.Errorf("asset sale policy has no asset ID: %w", - err) + var assetID asset.ID + + // We have performed checks for the asset IDs inside the HTLC against + // the specifier's group key in a previous step. Here we just need to + // provide a dummy value as the asset ID. The real asset IDs will be + // carefully picked in a later step in the process. What really matters + // now is the total amount. + switch { + case c.AssetSpecifier.HasGroupPubKey(): + groupKey := c.AssetSpecifier.UnwrapGroupKeyToPtr() + groupKeyX := schnorr.SerializePubKey(groupKey) + + assetID = asset.ID(groupKeyX) + + case c.AssetSpecifier.HasId(): + specifierID := *c.AssetSpecifier.UnwrapIdToPtr() + copy(assetID[:], specifierID[:]) } // Compute the outgoing asset amount given the msat outgoing amount and @@ -341,8 +355,9 @@ func NewAssetPurchasePolicy(quote rfqmsg.SellAccept) *AssetPurchasePolicy { // CheckHtlcCompliance returns an error if the given HTLC intercept descriptor // does not satisfy the subject policy. -func (c *AssetPurchasePolicy) CheckHtlcCompliance( - htlc lndclient.InterceptedHtlc) error { +func (c *AssetPurchasePolicy) CheckHtlcCompliance(ctx context.Context, + htlc lndclient.InterceptedHtlc, + specifierChecker rfqmsg.SpecifierChecker) error { // Since we will be reading CurrentAmountMsat value we acquire a read // lock. @@ -368,7 +383,9 @@ func (c *AssetPurchasePolicy) CheckHtlcCompliance( } // Sum the asset balance in the HTLC record. - assetAmt, err := htlcRecord.SumAssetBalance(c.AssetSpecifier) + assetAmt, err := htlcRecord.SumAssetBalance( + ctx, c.AssetSpecifier, specifierChecker, + ) if err != nil { return fmt.Errorf("error summing asset balance: %w", err) } @@ -523,15 +540,19 @@ func NewAssetForwardPolicy(incoming, outgoing Policy) (*AssetForwardPolicy, // CheckHtlcCompliance returns an error if the given HTLC intercept descriptor // does not satisfy the subject policy. -func (a *AssetForwardPolicy) CheckHtlcCompliance( - htlc lndclient.InterceptedHtlc) error { +func (a *AssetForwardPolicy) CheckHtlcCompliance(ctx context.Context, + htlc lndclient.InterceptedHtlc, sChk rfqmsg.SpecifierChecker) error { - if err := a.incomingPolicy.CheckHtlcCompliance(htlc); err != nil { + if err := a.incomingPolicy.CheckHtlcCompliance( + ctx, htlc, sChk, + ); err != nil { return fmt.Errorf("error checking forward policy, inbound "+ "HTLC does not comply with policy: %w", err) } - if err := a.outgoingPolicy.CheckHtlcCompliance(htlc); err != nil { + if err := a.outgoingPolicy.CheckHtlcCompliance( + ctx, htlc, sChk, + ); err != nil { return fmt.Errorf("error checking forward policy, outbound "+ "HTLC does not comply with policy: %w", err) } @@ -642,6 +663,10 @@ type OrderHandlerCfg struct { // HtlcSubscriber is a subscriber that is used to retrieve live HTLC // event updates. HtlcSubscriber HtlcSubscriber + + // SpecifierChecker is an interface that contains methods for + // checking certain properties related to asset specifiers. + SpecifierChecker rfqmsg.SpecifierChecker } // OrderHandler orchestrates management of accepted quote bundles. It monitors @@ -684,7 +709,7 @@ func NewOrderHandler(cfg OrderHandlerCfg) (*OrderHandler, error) { // // NOTE: This function must be thread safe. It is used by an external // interceptor service. -func (h *OrderHandler) handleIncomingHtlc(_ context.Context, +func (h *OrderHandler) handleIncomingHtlc(ctx context.Context, htlc lndclient.InterceptedHtlc) (*lndclient.InterceptedHtlcResponse, error) { @@ -716,7 +741,7 @@ func (h *OrderHandler) handleIncomingHtlc(_ context.Context, // At this point, we know that a policy exists and has not expired // whilst sitting in the local cache. We can now check that the HTLC // complies with the policy. - err = policy.CheckHtlcCompliance(htlc) + err = policy.CheckHtlcCompliance(ctx, htlc, h.cfg.SpecifierChecker) if err != nil { log.Warnf("HTLC does not comply with policy: %v "+ "(HTLC=%v, policy=%v)", err, htlc, policy) diff --git a/rfqmsg/records.go b/rfqmsg/records.go index c10f8b07e..bff555edc 100644 --- a/rfqmsg/records.go +++ b/rfqmsg/records.go @@ -2,6 +2,7 @@ package rfqmsg import ( "bytes" + "context" "encoding/hex" "encoding/json" "errors" @@ -82,22 +83,35 @@ func (h *Htlc) Balances() []*AssetBalance { return h.Amounts.Val.Balances } +// SpecifierChecker checks whether the passed specifier and asset ID match. If +// the specifier contains a group key, it will check whether the asset belongs +// to that group. +type SpecifierChecker func(ctx context.Context, specifier asset.Specifier, + id asset.ID) (bool, error) + // SumAssetBalance returns the sum of the asset balances for the given asset. -func (h *Htlc) SumAssetBalance(assetSpecifier asset.Specifier) (rfqmath.BigInt, +func (h *Htlc) SumAssetBalance(ctx context.Context, + assetSpecifier asset.Specifier, + specifierChecker SpecifierChecker) (rfqmath.BigInt, error) { balanceTotal := rfqmath.NewBigIntFromUint64(0) - targetAssetID, err := assetSpecifier.UnwrapIdOrErr() - if err != nil { - return balanceTotal, fmt.Errorf("unable to unwrap asset ID: %w", - err) + if specifierChecker == nil { + return balanceTotal, fmt.Errorf("checker is nil") } for idx := range h.Amounts.Val.Balances { balance := h.Amounts.Val.Balances[idx] - if balance.AssetID.Val != targetAssetID { + match, err := specifierChecker( + ctx, assetSpecifier, balance.AssetID.Val, + ) + if err != nil { + return balanceTotal, err + } + + if !match { continue } diff --git a/rfqmsg/records_test.go b/rfqmsg/records_test.go index 69c68bcc1..9898fb013 100644 --- a/rfqmsg/records_test.go +++ b/rfqmsg/records_test.go @@ -2,12 +2,14 @@ package rfqmsg import ( "bytes" + "context" "encoding/json" "testing" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) @@ -23,6 +25,22 @@ type htlcTestCase struct { sumBalances map[asset.ID]rfqmath.BigInt } +type DummyChecker struct{} + +func MockAssetMatchesSpecifier(_ context.Context, specifier asset.Specifier, + id asset.ID) (bool, error) { + + switch { + case specifier.HasGroupPubKey(): + return true, nil + + case specifier.HasId(): + return *specifier.UnwrapIdToPtr() == id, nil + } + + return false, nil +} + // assetHtlcTestCase is a helper function that asserts different properties of // the test case. func assetHtlcTestCase(t *testing.T, tc htlcTestCase) { @@ -63,7 +81,14 @@ func assetHtlcTestCase(t *testing.T, tc htlcTestCase) { for assetID, expectedBalance := range tc.sumBalances { assetSpecifier := asset.NewSpecifierFromId(assetID) - balance, err := tc.htlc.SumAssetBalance(assetSpecifier) + + ctxt, cancel := context.WithTimeout( + context.Background(), wait.DefaultTimeout, + ) + defer cancel() + balance, err := tc.htlc.SumAssetBalance( + ctxt, assetSpecifier, MockAssetMatchesSpecifier, + ) require.NoError(t, err) require.Equal(t, expectedBalance, balance) diff --git a/rpcserver.go b/rpcserver.go index 2b765fbe0..9f64d5384 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -6,12 +6,12 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" - "encoding/json" "errors" "fmt" "io" "math" "net/http" + "sort" "strings" "sync" "sync/atomic" @@ -6432,6 +6432,31 @@ func MarshalAssetFedSyncCfg( }, nil } +// marshalAssetSpecifier marshals an asset specifier to the RPC form. +func marshalAssetSpecifier(specifier asset.Specifier) rfqrpc.AssetSpecifier { + switch { + case specifier.HasId(): + assetID := specifier.UnwrapIdToPtr() + return rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: assetID[:], + }, + } + + case specifier.HasGroupPubKey(): + groupKey := specifier.UnwrapGroupKeyToPtr() + groupKeyBytes := groupKey.SerializeCompressed() + return rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_GroupKey{ + GroupKey: groupKeyBytes, + }, + } + + default: + return rfqrpc.AssetSpecifier{} + } +} + // unmarshalAssetSpecifier unmarshals an asset specifier from the RPC form. func unmarshalAssetSpecifier(s *rfqrpc.AssetSpecifier) (*asset.ID, *btcec.PublicKey, error) { @@ -6629,7 +6654,7 @@ func (r *rpcServer) AddAssetBuyOrder(ctx context.Context, for { select { case event := <-eventSubscriber.NewItemCreated.ChanOut(): - resp, err := taprpc.NewAddAssetBuyOrderResponse(event) + resp, err := rfq.NewAddAssetBuyOrderResponse(event) if err != nil { return nil, fmt.Errorf("error marshalling "+ "buy order response: %w", err) @@ -6682,7 +6707,10 @@ func (r *rpcServer) checkPeerChannel(ctx context.Context, peer route.Vertex, // If we don't get an error here, it means we do have an asset // channel with the peer. The intention doesn't matter as we're // just checking whether a channel exists. - _, err := r.rfqChannel(ctx, specifier, &peer, NoIntention) + _, err := r.cfg.RfqManager.RfqChannel( + ctx, r.cfg.Lnd.Client.ListChannels, specifier, &peer, + rfq.NoIntention, + ) if err != nil { return fmt.Errorf("error checking asset channel: %w", err) @@ -6803,7 +6831,7 @@ func (r *rpcServer) AddAssetSellOrder(ctx context.Context, for { select { case event := <-eventSubscriber.NewItemCreated.ChanOut(): - resp, err := taprpc.NewAddAssetSellOrderResponse(event) + resp, err := rfq.NewAddAssetSellOrderResponse(event) if err != nil { return nil, fmt.Errorf("error marshalling "+ "sell order response: %w", err) @@ -6991,7 +7019,7 @@ func marshallRfqEvent(eventInterface fn.Event) (*rfqrpc.RfqEvent, error) { switch event := eventInterface.(type) { case *rfq.PeerAcceptedBuyQuoteEvent: - acceptedQuote, err := taprpc.MarshalAcceptedBuyQuoteEvent(event) + acceptedQuote, err := rfq.MarshalAcceptedBuyQuoteEvent(event) if err != nil { return nil, err } @@ -7007,7 +7035,7 @@ func marshallRfqEvent(eventInterface fn.Event) (*rfqrpc.RfqEvent, error) { }, nil case *rfq.PeerAcceptedSellQuoteEvent: - rpcAcceptedQuote := taprpc.MarshalAcceptedSellQuoteEvent( + rpcAcceptedQuote := rfq.MarshalAcceptedSellQuoteEvent( event, ) @@ -7221,19 +7249,32 @@ func (r *rpcServer) EncodeCustomRecords(_ context.Context, func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest, stream tchrpc.TaprootAssetChannels_SendPaymentServer) error { + if len(req.AssetId) > 0 && len(req.GroupKey) > 0 { + return fmt.Errorf("cannot set both asset id and group key") + } + if req.PaymentRequest == nil { return fmt.Errorf("payment request must be specified") } pReq := req.PaymentRequest ctx := stream.Context() - // Do some preliminary checks on the asset ID and make sure we have any - // balance for that asset. - if len(req.AssetId) != sha256.Size { - return fmt.Errorf("asset ID must be 32 bytes") + var specifier asset.Specifier + + assetID, groupKey, err := parseAssetSpecifier( + req.AssetId, "", req.GroupKey, "", + ) + if err != nil { + return err + } + + switch { + case assetID != nil: + specifier = asset.NewSpecifierFromId(*assetID) + + case groupKey != nil: + specifier = asset.NewSpecifierFromGroupKey(*groupKey) } - var assetID asset.ID - copy(assetID[:], req.AssetId) // Now that we know we have at least _some_ asset balance, we'll figure // out what kind of payment this is, so we can determine _how many_ @@ -7296,7 +7337,7 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest, // Calculate the equivalent asset units for the given invoice // amount based on the asset-to-BTC conversion rate. - sellOrder := taprpc.MarshalAcceptedSellQuote(*quote) + sellOrder := rfq.MarshalAcceptedSellQuote(*quote) // paymentMaxAmt is the maximum amount that the counterparty is // expected to pay. This is the amount that the invoice is @@ -7358,21 +7399,34 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest, peerPubKey = &parsedKey } - specifier := asset.NewSpecifierFromId(assetID) + rpcSpecifier := marshalAssetSpecifier(specifier) // We can now query the asset channels we have. - assetChan, err := r.rfqChannel( - ctx, specifier, peerPubKey, SendIntention, + chanMap, err := r.cfg.RfqManager.RfqChannel( + ctx, r.cfg.Lnd.Client.ListChannels, specifier, + peerPubKey, rfq.SendIntention, ) if err != nil { return fmt.Errorf("error finding asset channel to "+ "use: %w", err) } + // TODO(george): temporary as multi-rfq send is not supported + // yet + if peerPubKey == nil && len(chanMap) > 1 { + return fmt.Errorf("multiple valid peers found, need " + + "specify peer pub key") + } // Even if the user didn't specify the peer public key before, // we definitely know it now. So let's make sure it's always // set. - peerPubKey = &assetChan.channelInfo.PubKeyBytes + // + // TODO(george): we just grab the first value, this is temporary + // until multi-rfq send is implemented. + for _, v := range chanMap { + peerPubKey = &v[0].ChannelInfo.PubKeyBytes + break + } // paymentMaxAmt is the maximum amount that the counterparty is // expected to pay. This is the amount that the invoice is @@ -7384,14 +7438,10 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest, resp, err := r.AddAssetSellOrder( ctx, &rfqrpc.AddAssetSellOrderRequest{ - AssetSpecifier: &rfqrpc.AssetSpecifier{ - Id: &rfqrpc.AssetSpecifier_AssetId{ - AssetId: assetID[:], - }, - }, - PaymentMaxAmt: uint64(paymentMaxAmt), - Expiry: uint64(expiry.Unix()), - PeerPubKey: peerPubKey[:], + AssetSpecifier: &rpcSpecifier, + PaymentMaxAmt: uint64(paymentMaxAmt), + Expiry: uint64(expiry.Unix()), + PeerPubKey: peerPubKey[:], TimeoutSeconds: uint32( rfq.DefaultTimeout.Seconds(), ), @@ -7474,9 +7524,32 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest, "for keysend payment") } - balances := []*rfqmsg.AssetBalance{ - rfqmsg.NewAssetBalance(assetID, req.AssetAmount), + var balances []*rfqmsg.AssetBalance + + switch { + case specifier.HasId(): + balances = []*rfqmsg.AssetBalance{ + rfqmsg.NewAssetBalance( + *specifier.UnwrapIdToPtr(), + req.AssetAmount, + ), + } + + case specifier.HasGroupPubKey(): + groupKey := specifier.UnwrapGroupKeyToPtr() + groupKeyX := schnorr.SerializePubKey(groupKey) + + // We can't distribute the amount over distinct asset ID + // balances, so we provide the total amount under the + // dummy asset ID that is produced by hashing the group + // key. + balances = []*rfqmsg.AssetBalance{ + rfqmsg.NewAssetBalance( + asset.ID(groupKeyX), req.AssetAmount, + ), + } } + htlc := rfqmsg.NewHtlc(balances, fn.None[rfqmsg.ID]()) // We'll now map the HTLC struct into a set of TLV records, @@ -7627,18 +7700,31 @@ func checkOverpayment(quote *rfqrpc.PeerAcceptedSellQuote, func (r *rpcServer) AddInvoice(ctx context.Context, req *tchrpc.AddInvoiceRequest) (*tchrpc.AddInvoiceResponse, error) { + if len(req.AssetId) > 0 && len(req.GroupKey) > 0 { + return nil, fmt.Errorf("cannot set both asset id and group key") + } + if req.InvoiceRequest == nil { return nil, fmt.Errorf("invoice request must be specified") } iReq := req.InvoiceRequest - // Do some preliminary checks on the asset ID and make sure we have any - // balance for that asset. - if len(req.AssetId) != sha256.Size { - return nil, fmt.Errorf("asset ID must be 32 bytes") + var specifier asset.Specifier + + assetID, groupKey, err := parseAssetSpecifier( + req.AssetId, "", req.GroupKey, "", + ) + if err != nil { + return nil, err + } + + switch { + case assetID != nil: + specifier = asset.NewSpecifierFromId(*assetID) + + case groupKey != nil: + specifier = asset.NewSpecifierFromGroupKey(*groupKey) } - var assetID asset.ID - copy(assetID[:], req.AssetId) // The peer public key is optional if there is only a single asset // channel. @@ -7653,21 +7739,16 @@ func (r *rpcServer) AddInvoice(ctx context.Context, peerPubKey = &parsedKey } - specifier := asset.NewSpecifierFromId(assetID) - // We can now query the asset channels we have. - assetChan, err := r.rfqChannel( - ctx, specifier, peerPubKey, ReceiveIntention, + chanMap, err := r.cfg.RfqManager.RfqChannel( + ctx, r.cfg.Lnd.Client.ListChannels, specifier, peerPubKey, + rfq.ReceiveIntention, ) if err != nil { return nil, fmt.Errorf("error finding asset channel to use: %w", err) } - // Even if the user didn't specify the peer public key before, we - // definitely know it now. So let's make sure it's always set. - peerPubKey = &assetChan.channelInfo.PubKeyBytes - expirySeconds := iReq.Expiry if expirySeconds == 0 { expirySeconds = int64(rfq.DefaultInvoiceExpiry.Seconds()) @@ -7676,85 +7757,64 @@ func (r *rpcServer) AddInvoice(ctx context.Context, time.Duration(expirySeconds) * time.Second, ) - resp, err := r.AddAssetBuyOrder(ctx, &rfqrpc.AddAssetBuyOrderRequest{ - AssetSpecifier: &rfqrpc.AssetSpecifier{ - Id: &rfqrpc.AssetSpecifier_AssetId{ - AssetId: assetID[:], - }, - }, - AssetMaxAmt: req.AssetAmount, - Expiry: uint64(expiryTimestamp.Unix()), - PeerPubKey: peerPubKey[:], - TimeoutSeconds: uint32( - rfq.DefaultTimeout.Seconds(), - ), - }) - if err != nil { - return nil, fmt.Errorf("error adding buy order: %w", err) + rpcSpecifier := marshalAssetSpecifier(specifier) + + type quoteWithInfo struct { + quote *rfqrpc.PeerAcceptedBuyQuote + rate *rfqmath.BigIntFixedPoint + channel rfq.ChannelWithSpecifier } - var acceptedQuote *rfqrpc.PeerAcceptedBuyQuote - switch r := resp.Response.(type) { - case *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote: - acceptedQuote = r.AcceptedQuote + acquiredQuotes := make([]quoteWithInfo, 0) - case *rfqrpc.AddAssetBuyOrderResponse_InvalidQuote: - return nil, fmt.Errorf("peer %v sent back an invalid quote, "+ - "status: %v", r.InvalidQuote.Peer, - r.InvalidQuote.Status.String()) + // TODO(george): should we limit the max number of acquired quotes? + // Tapd nodes with hundreds or more of chans might face a bottleneck + // here. Could just acquire quotes that reach a certain threshold. + for peer, channels := range chanMap { + quote, err := r.AcquireBuyOrder( + ctx, &rpcSpecifier, req.AssetAmount, expiryTimestamp, + &peer, + ) + if err != nil { + // TODO(george): should handle negotiation failures + // gracefully. + return nil, err + } - case *rfqrpc.AddAssetBuyOrderResponse_RejectedQuote: - return nil, fmt.Errorf("peer %v rejected the quote, code: %v, "+ - "error message: %v", r.RejectedQuote.Peer, - r.RejectedQuote.ErrorCode, r.RejectedQuote.ErrorMessage) + rate, err := rfqrpc.UnmarshalFixedPoint(quote.AskAssetRate) + if err != nil { + return nil, err + } - default: - return nil, fmt.Errorf("unexpected response type: %T", r) + acquiredQuotes = append(acquiredQuotes, quoteWithInfo{ + quote: quote, + rate: rate, + // Since the channels are sorted, we now the value with + // the greatest remote balance is at index 0. + channel: channels[0], + }) } - // If the invoice is for an asset unit amount smaller than the minimal - // transportable amount, we'll return an error, as it wouldn't be - // payable by the network. - if acceptedQuote.MinTransportableUnits > req.AssetAmount { - return nil, fmt.Errorf("cannot create invoice over %d asset "+ - "units, as the minimal transportable amount is %d "+ - "units with the current rate of %v units/BTC", - req.AssetAmount, acceptedQuote.MinTransportableUnits, - acceptedQuote.AskAssetRate) - } + // Let's sort the ask rate of the quotes in ascending order. + sort.Slice(acquiredQuotes, func(i, j int) bool { + return acquiredQuotes[i].rate.ToUint64() < + acquiredQuotes[j].rate.ToUint64() + }) - // Now that we have the accepted quote, we know the amount in Satoshi - // that we need to pay. We can now update the invoice with this amount. - // - // First, un-marshall the ask asset rate from the accepted quote. - askAssetRate, err := rfqrpc.UnmarshalFixedPoint( - acceptedQuote.AskAssetRate, - ) - if err != nil { - return nil, fmt.Errorf("error unmarshalling ask asset rate: %w", - err) - } + // We grab the most expensive rate to use as reference for the total + // invoice amount. Since peers have varying prices for the assets, we + // pick the most expensive rate in order to allow for any combination of + // MPP shards through our set of chosen peers. + expensiveRate := acquiredQuotes[0].rate + expensiveQuote := acquiredQuotes[0].quote // Convert the asset amount into a fixed-point. assetAmount := rfqmath.NewBigIntFixedPoint(req.AssetAmount, 0) // Calculate the invoice amount in msat. - valMsat := rfqmath.UnitsToMilliSatoshi(assetAmount, *askAssetRate) + valMsat := rfqmath.UnitsToMilliSatoshi(assetAmount, *expensiveRate) iReq.ValueMsat = int64(valMsat) - // The last step is to create a hop hint that includes the fake SCID of - // the quote, alongside the channel's routing policy. We need to choose - // the policy that points towards us, as the payment will be flowing in. - // So we get the policy that's being set by the remote peer. - channelID := assetChan.channelInfo.ChannelID - inboundPolicy, err := r.getInboundPolicy( - ctx, channelID, peerPubKey.String(), - ) - if err != nil { - return nil, fmt.Errorf("unable to get inbound channel policy "+ - "for channel with ID %d: %w", channelID, err) - } - // If this is a hodl invoice, then we'll copy over the relevant fields, // then route this through the invoicerpc instead. if req.HodlInvoice != nil { @@ -7764,24 +7824,19 @@ func (r *rpcServer) AddInvoice(ctx context.Context, "hash: %w", err) } - peerPub, err := btcec.ParsePubKey(peerPubKey[:]) - if err != nil { - return nil, fmt.Errorf("error parsing peer "+ - "pubkey: %w", err) - } + routeHints := make([][]zpay32.HopHint, 0) + for _, v := range acquiredQuotes { + _, hopHint, err := r.cfg.RfqManager.RfqToHopHint( + ctx, r.getInboundPolicy, + v.channel.ChannelInfo.ChannelID, + v.channel.ChannelInfo.PubKeyBytes, v.quote, + true, + ) + if err != nil { + return nil, err + } - hopHint := []zpay32.HopHint{ - { - NodeID: peerPub, - ChannelID: acceptedQuote.Scid, - FeeBaseMSat: uint32(inboundPolicy.FeeBaseMsat), - FeeProportionalMillionths: uint32( - inboundPolicy.FeeRateMilliMsat, - ), - CLTVExpiryDelta: uint16( - inboundPolicy.TimeLockDelta, - ), - }, + routeHints = append(routeHints, hopHint) } payReq, err := r.cfg.Lnd.Invoices.AddHoldInvoice( @@ -7797,7 +7852,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context, // add any hop hints other than this one. Private: false, HodlInvoice: true, - RouteHints: [][]zpay32.HopHint{hopHint}, + RouteHints: routeHints, }, ) if err != nil { @@ -7806,31 +7861,35 @@ func (r *rpcServer) AddInvoice(ctx context.Context, } return &tchrpc.AddInvoiceResponse{ - AcceptedBuyQuote: acceptedQuote, + // TODO(george): For now we just return the expensive + // quote + AcceptedBuyQuote: expensiveQuote, InvoiceResult: &lnrpc.AddInvoiceResponse{ PaymentRequest: payReq, }, }, nil } - // Otherwise, we'll make this into a normal invoice. - hopHint := &lnrpc.HopHint{ - NodeId: peerPubKey.String(), - ChanId: acceptedQuote.Scid, - FeeBaseMsat: uint32(inboundPolicy.FeeBaseMsat), - FeeProportionalMillionths: uint32( - inboundPolicy.FeeRateMilliMsat, - ), - CltvExpiryDelta: inboundPolicy.TimeLockDelta, - } - iReq.RouteHints = []*lnrpc.RouteHint{ - { + routeHints := make([]*lnrpc.RouteHint, 0) + for _, v := range acquiredQuotes { + hopHint, _, err := r.cfg.RfqManager.RfqToHopHint( + ctx, r.getInboundPolicy, + v.channel.ChannelInfo.ChannelID, + v.channel.ChannelInfo.PubKeyBytes, v.quote, false, + ) + if err != nil { + return nil, err + } + + routeHints = append(routeHints, &lnrpc.RouteHint{ HopHints: []*lnrpc.HopHint{ hopHint, }, - }, + }) } + iReq.RouteHints = routeHints + rpcCtx, _, rawClient := r.cfg.Lnd.Client.RawClientWithMacAuth(ctx) invoiceResp, err := rawClient.AddInvoice(rpcCtx, iReq) if err != nil { @@ -7838,11 +7897,64 @@ func (r *rpcServer) AddInvoice(ctx context.Context, } return &tchrpc.AddInvoiceResponse{ - AcceptedBuyQuote: acceptedQuote, + // TODO(george): For now we just return the expensive quote. + AcceptedBuyQuote: expensiveQuote, InvoiceResult: invoiceResp, }, nil } +func (r *rpcServer) AcquireBuyOrder(ctx context.Context, + rpcSpecifier *rfqrpc.AssetSpecifier, assetMaxAmt uint64, + expiryTimestamp time.Time, + peerPubKey *route.Vertex) (*rfqrpc.PeerAcceptedBuyQuote, error) { + + var quote *rfqrpc.PeerAcceptedBuyQuote + + resp, err := r.AddAssetBuyOrder(ctx, &rfqrpc.AddAssetBuyOrderRequest{ + AssetSpecifier: rpcSpecifier, + AssetMaxAmt: assetMaxAmt, + Expiry: uint64(expiryTimestamp.Unix()), + PeerPubKey: peerPubKey[:], + TimeoutSeconds: uint32( + rfq.DefaultTimeout.Seconds(), + ), + }) + if err != nil { + return quote, fmt.Errorf("error adding buy order: %w", err) + } + + switch r := resp.Response.(type) { + case *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote: + quote = r.AcceptedQuote + + case *rfqrpc.AddAssetBuyOrderResponse_InvalidQuote: + return nil, fmt.Errorf("peer %v sent back an invalid quote, "+ + "status: %v", r.InvalidQuote.Peer, + r.InvalidQuote.Status.String()) + + case *rfqrpc.AddAssetBuyOrderResponse_RejectedQuote: + return nil, fmt.Errorf("peer %v rejected the quote, code: %v, "+ + "error message: %v", r.RejectedQuote.Peer, + r.RejectedQuote.ErrorCode, r.RejectedQuote.ErrorMessage) + + default: + return nil, fmt.Errorf("unexpected response type: %T", r) + } + + // If the invoice is for an asset unit amount smaller than the minimal + // transportable amount, we'll return an error, as it wouldn't be + // payable by the network. + if quote.MinTransportableUnits > assetMaxAmt { + return nil, fmt.Errorf("cannot create invoice over %d asset "+ + "units, as the minimal transportable amount is %d "+ + "units with the current rate of %v units/BTC", + assetMaxAmt, quote.MinTransportableUnits, + quote.AskAssetRate) + } + + return quote, nil +} + // DeclareScriptKey declares a new script key to the wallet. This is useful // when the script key contains scripts, which would mean it wouldn't be // recognized by the wallet automatically. Declaring a script key will make any @@ -7925,177 +8037,6 @@ func (r *rpcServer) DecDisplayForAssetID(ctx context.Context, return meta.DecDisplayOption() } -// chanIntention defines the intention of calling rfqChannel. This helps with -// returning the channel that is most suitable for what we want to do. -type chanIntention uint8 - -const ( - // NoIntention defines the absence of any intention, signalling that we - // don't really care which channel is returned. - NoIntention chanIntention = iota - - // SendIntention defines the intention to send over an asset channel. - SendIntention - - // ReceiveIntention defines the intention to receive over an asset - // channel. - ReceiveIntention -) - -// rfqChannel returns the channel to use for RFQ operations. If a peer public -// key is specified, the channels are filtered by that peer. If there are -// multiple channels for the same specifier, the user must specify the peer -// public key. -func (r *rpcServer) rfqChannel(ctx context.Context, specifier asset.Specifier, - peerPubKey *route.Vertex, - intention chanIntention) (*channelWithSpecifier, error) { - - balances, err := r.computeChannelAssetBalance(ctx, specifier) - if err != nil { - return nil, fmt.Errorf("error computing available asset "+ - "channel balance: %w", err) - } - - if len(balances) == 0 { - return nil, fmt.Errorf("no asset channel balance found for %s", - &specifier) - } - - // If a peer public key was specified, we always want to use that to - // filter the asset channels. - if peerPubKey != nil { - balances = fn.Filter( - balances, func(c channelWithSpecifier) bool { - return c.channelInfo.PubKeyBytes == *peerPubKey - }, - ) - } - - switch { - // If there are multiple asset channels for the same specifier, we need - // to ask the user to specify the peer public key. Otherwise, we don't - // know who to ask for a quote. - case len(balances) > 1 && peerPubKey == nil: - return nil, fmt.Errorf("multiple asset channels found for "+ - "%s, please specify the peer pubkey", &specifier) - - // We don't have any channels with that asset ID and peer. - case len(balances) == 0: - return nil, fmt.Errorf("no asset channel found for %s", - &specifier) - } - - // If the user specified a peer public key, and we still have multiple - // channels, it means we have multiple channels with the same asset and - // the same peer, as we ruled out the rest of the cases above. - - // Initialize best balance to first channel of the list. - bestBalance := balances[0] - - switch intention { - case ReceiveIntention: - // If the intention is to receive, return the channel - // with the best remote balance. - fn.ForEach(balances, func(b channelWithSpecifier) { - if b.assetInfo.RemoteBalance > - bestBalance.assetInfo.RemoteBalance { - - bestBalance = b - } - }) - - case SendIntention: - // If the intention is to send, return the channel with - // the best local balance. - fn.ForEach(balances, func(b channelWithSpecifier) { - if b.assetInfo.LocalBalance > - bestBalance.assetInfo.LocalBalance { - - bestBalance = b - } - }) - - case NoIntention: - // Do nothing. Just return the first element that was - // assigned above. - } - - return &bestBalance, nil -} - -// channelWithSpecifier is a helper struct that combines the information of an -// asset specifier that is satisfied by a channel with the channels' general -// information. -type channelWithSpecifier struct { - // specifier is the asset specifier that is satisfied by this channels' - // assets. - specifier asset.Specifier - - // channelInfo is the information about the channel the asset is - // committed to. - channelInfo lndclient.ChannelInfo - - // assetInfo contains the asset related info of the channel. - assetInfo rfqmsg.JsonAssetChanInfo -} - -// computeChannelAssetBalance computes the total local and remote balance for -// each asset channel that matches the provided asset specifier. -func (r *rpcServer) computeChannelAssetBalance(ctx context.Context, - specifier asset.Specifier) ([]channelWithSpecifier, error) { - - activeChannels, err := r.cfg.Lnd.Client.ListChannels(ctx, true, false) - if err != nil { - return nil, fmt.Errorf("unable to fetch channels: %w", err) - } - - channels := make([]channelWithSpecifier, 0) - for chanIdx := range activeChannels { - openChan := activeChannels[chanIdx] - if len(openChan.CustomChannelData) == 0 { - continue - } - - var assetData rfqmsg.JsonAssetChannel - err = json.Unmarshal(openChan.CustomChannelData, &assetData) - if err != nil { - return nil, fmt.Errorf("unable to unmarshal asset "+ - "data: %w", err) - } - - // Check if the assets of this channel match the provided - // specifier. - pass, err := r.cfg.RfqManager.ChannelCompatible( - ctx, assetData.Assets, specifier, - ) - if err != nil { - return nil, err - } - - if pass { - // Since the assets of the channel passed the above - // filter, we're safe to aggregate their info to be - // represented as a single entity. - var aggrInfo rfqmsg.JsonAssetChanInfo - - // TODO(george): refactor when JSON gets fixed - for _, info := range assetData.Assets { - aggrInfo.Capacity += info.Capacity - aggrInfo.LocalBalance += info.LocalBalance - aggrInfo.RemoteBalance += info.RemoteBalance - } - - channels = append(channels, channelWithSpecifier{ - specifier: specifier, - channelInfo: openChan, - assetInfo: aggrInfo, - }) - } - } - - return channels, nil -} - // getInboundPolicy returns the policy of the given channel that points towards // our node, so it's the policy set by the remote peer. func (r *rpcServer) getInboundPolicy(ctx context.Context, chanID uint64, diff --git a/tapchannel/aux_invoice_manager.go b/tapchannel/aux_invoice_manager.go index 511804233..92be86be7 100644 --- a/tapchannel/aux_invoice_manager.go +++ b/tapchannel/aux_invoice_manager.go @@ -42,6 +42,12 @@ type RfqManager interface { // node and have been requested by our peers. These quotes are // exclusively available to our node for the sale of assets. LocalAcceptedSellQuotes() rfq.SellAcceptMap + + // AssetMatchesSpecifier checks if the provided asset satisfies the + // provided specifier. If the specifier includes a group key, we will + // check if the asset belongs to that group. + AssetMatchesSpecifier(ctx context.Context, specifier asset.Specifier, + id asset.ID) (bool, error) } // A compile time assertion to ensure that the rfq.Manager meets the expected @@ -128,7 +134,7 @@ func (s *AuxInvoiceManager) Start() error { // handleInvoiceAccept is the handler that will be called for each invoice that // is accepted. It will intercept the HTLCs that attempt to settle the invoice // and modify them if necessary. -func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context, +func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context, req lndclient.InvoiceHtlcModifyRequest) ( *lndclient.InvoiceHtlcModifyResponse, error) { @@ -200,7 +206,7 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context, } // We now run some validation checks on the asset HTLC. - err = s.validateAssetHTLC(htlc) + err = s.validateAssetHTLC(ctx, htlc) if err != nil { log.Errorf("Failed to validate asset HTLC: %v", err) @@ -274,22 +280,23 @@ func (s *AuxInvoiceManager) identifierFromQuote( buyQuote, isBuy := acceptedBuyQuotes[rfqID.Scid()] sellQuote, isSell := acceptedSellQuotes[rfqID.Scid()] + var specifier asset.Specifier + switch { case isBuy: - if buyQuote.Request.AssetSpecifier.HasId() { - req := buyQuote.Request - return req.AssetSpecifier, nil - } + specifier = buyQuote.Request.AssetSpecifier case isSell: - if sellQuote.Request.AssetSpecifier.HasId() { - req := sellQuote.Request - return req.AssetSpecifier, nil - } + specifier = sellQuote.Request.AssetSpecifier + } + + err := specifier.AssertNotEmpty() + if err != nil { + return specifier, fmt.Errorf("rfqID does not match any "+ + "accepted buy or sell quote: %v", err) } - return asset.Specifier{}, fmt.Errorf("rfqID does not match any " + - "accepted buy or sell quote") + return specifier, nil } // priceFromQuote retrieves the price from the accepted quote for the given RFQ @@ -382,7 +389,9 @@ func isAssetInvoice(invoice *lnrpc.Invoice, rfqLookup RfqLookup) bool { } // validateAssetHTLC runs a couple of checks on the provided asset HTLC. -func (s *AuxInvoiceManager) validateAssetHTLC(htlc *rfqmsg.Htlc) error { +func (s *AuxInvoiceManager) validateAssetHTLC(ctx context.Context, + htlc *rfqmsg.Htlc) error { + rfqID := htlc.RfqID.ValOpt().UnsafeFromSome() // Retrieve the asset identifier from the RFQ quote. @@ -392,27 +401,20 @@ func (s *AuxInvoiceManager) validateAssetHTLC(htlc *rfqmsg.Htlc) error { "quote: %v", err) } - if !identifier.HasId() { - return fmt.Errorf("asset specifier has empty assetID") - } - // Check for each of the asset balances of the HTLC that the identifier // matches that of the RFQ quote. for _, v := range htlc.Balances() { - err := fn.MapOptionZ( - identifier.ID(), func(id asset.ID) error { - if v.AssetID.Val != id { - return fmt.Errorf("mismatch between " + - "htlc asset ID and rfq asset " + - "ID") - } - - return nil - }, + match, err := s.cfg.RfqManager.AssetMatchesSpecifier( + ctx, identifier, v.AssetID.Val, ) if err != nil { return err } + + if !match { + return fmt.Errorf("asset ID %s does not match %s", + v.AssetID.Val.String(), identifier.String()) + } } return nil diff --git a/tapchannel/aux_invoice_manager_test.go b/tapchannel/aux_invoice_manager_test.go index e180ca17b..e04132bc6 100644 --- a/tapchannel/aux_invoice_manager_test.go +++ b/tapchannel/aux_invoice_manager_test.go @@ -63,6 +63,22 @@ var ( // The test RFQ SCID that is derived from testRfqID. testScid = testRfqID.Scid() + + // The common asset ID used on test cases. + assetID = dummyAssetID(1) + + // The asset ID of the first asset that is considered part of the group. + groupAssetID1 = dummyAssetID(41) + + // The asset ID of the second asset that is considered part of the + // group. + groupAssetID2 = dummyAssetID(42) + + // The specifier based on the common asset ID. + assetSpecifier = asset.NewSpecifierFromId(assetID) + + // The specifier based on a dummy generated group key. + groupSpecifier = asset.NewSpecifierFromGroupKey(*pubKeyFromUint64(1337)) ) // mockRfqManager mocks the interface of the rfq manager required by the aux @@ -73,14 +89,41 @@ type mockRfqManager struct { localSellQuotes rfq.SellAcceptMap } +// PeerAcceptedBuyQuotes returns buy quotes that were requested by our node and +// have been accepted by our peers. These quotes are exclusively available to +// our node for the acquisition of assets. func (m *mockRfqManager) PeerAcceptedBuyQuotes() rfq.BuyAcceptMap { return m.peerBuyQuotes } +// LocalAcceptedSellQuotes returns sell quotes that were accepted by our node +// and have been requested by our peers. These quotes are exclusively available +// to our node for the sale of assets. func (m *mockRfqManager) LocalAcceptedSellQuotes() rfq.SellAcceptMap { return m.localSellQuotes } +// AssetMatchesSpecifier checks if the provided asset satisfies the provided +// specifier. If the specifier includes a group key, we will check if the asset +// belongs to that group. +func (m *mockRfqManager) AssetMatchesSpecifier(ctx context.Context, + specifier asset.Specifier, id asset.ID) (bool, error) { + + switch { + case specifier.HasGroupPubKey(): + if id == groupAssetID1 || id == groupAssetID2 { + return true, nil + } + + return false, nil + + case specifier.HasId(): + return *specifier.UnwrapIdToPtr() == id, nil + } + + return false, nil +} + // mockHtlcModifier mocks the HtlcModifier interface that is required by the // AuxInvoiceManager. type mockHtlcModifier struct { @@ -100,12 +143,12 @@ func (m *mockHtlcModifier) HtlcModifier(ctx context.Context, res, err := handler(ctx, r) if err != nil { - return err + m.t.Errorf("Handler error: %v", err) } if m.expectedResQue[i].CancelSet { if !res.CancelSet { - return fmt.Errorf("expected cancel set flag") + m.t.Errorf("expected cancel set flag") } continue @@ -113,7 +156,7 @@ func (m *mockHtlcModifier) HtlcModifier(ctx context.Context, // Check if there's a match with the expected outcome. if res.AmtPaid != m.expectedResQue[i].AmtPaid { - return fmt.Errorf("invoice paid amount does not match "+ + m.t.Errorf("invoice paid amount does not match "+ "expected amount, %v != %v", res.AmtPaid, m.expectedResQue[i].AmtPaid) } @@ -269,11 +312,6 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context, // TestAuxInvoiceManager tests that the htlc modifications of the aux invoice // manager align with our expectations. func TestAuxInvoiceManager(t *testing.T) { - var ( - assetID = dummyAssetID(1) - assetSpecifier = asset.NewSpecifierFromId(assetID) - ) - testCases := []struct { name string buyQuotes rfq.BuyAcceptMap @@ -471,6 +509,94 @@ func TestAuxInvoiceManager(t *testing.T) { }, }, }, + { + name: "asset invoice, group key rfq", + requests: []lndclient.InvoiceHtlcModifyRequest{ + { + Invoice: &lnrpc.Invoice{ + RouteHints: testRouteHints(), + ValueMsat: 20_000_000, + PaymentAddr: []byte{1, 1, 1}, + }, + WireCustomRecords: newWireCustomRecords( + t, []*rfqmsg.AssetBalance{ + // Balance asset ID + // belongs to group. + rfqmsg.NewAssetBalance( + groupAssetID1, + 3, + ), + // Balance asset ID + // belongs to group. + rfqmsg.NewAssetBalance( + groupAssetID2, + 4, + ), + }, fn.Some(testRfqID), + ), + }, + }, + responses: []lndclient.InvoiceHtlcModifyResponse{ + { + AmtPaid: 3_000_000 + 4_000_000, + }, + }, + buyQuotes: rfq.BuyAcceptMap{ + testScid: { + Peer: testNodeID, + AssetRate: rfqmsg.NewAssetRate( + testAssetRate, time.Now(), + ), + Request: rfqmsg.BuyRequest{ + AssetSpecifier: groupSpecifier, + }, + }, + }, + }, + { + name: "asset invoice, group key rfq, bad htlc", + requests: []lndclient.InvoiceHtlcModifyRequest{ + { + Invoice: &lnrpc.Invoice{ + RouteHints: testRouteHints(), + ValueMsat: 20_000_000, + PaymentAddr: []byte{1, 1, 1}, + }, + WireCustomRecords: newWireCustomRecords( + t, []*rfqmsg.AssetBalance{ + // Balance asset ID does + // not belong to group. + rfqmsg.NewAssetBalance( + dummyAssetID(2), + 3, + ), + // Balance asset ID does + // not belong to group. + rfqmsg.NewAssetBalance( + dummyAssetID(3), + 4, + ), + }, fn.Some(testRfqID), + ), + }, + }, + responses: []lndclient.InvoiceHtlcModifyResponse{ + { + CancelSet: true, + }, + }, + buyQuotes: rfq.BuyAcceptMap{ + testScid: { + Peer: testNodeID, + AssetRate: rfqmsg.NewAssetRate( + testAssetRate, time.Now(), + ), + Request: rfqmsg.BuyRequest{ + AssetSpecifier: groupSpecifier, + }, + }, + }, + }, } for _, testCase := range testCases { diff --git a/tapchannel/aux_traffic_shaper.go b/tapchannel/aux_traffic_shaper.go index ef3fe93b8..cc5d61e1f 100644 --- a/tapchannel/aux_traffic_shaper.go +++ b/tapchannel/aux_traffic_shaper.go @@ -4,8 +4,10 @@ import ( "fmt" "sync" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/rfqmath" @@ -308,11 +310,20 @@ func (s *AuxTrafficShaper) ProduceHtlcExtraData(totalAmount lnwire.MilliSatoshi, ) numAssetUnits := numAssetUnitsFp.ScaleTo(0).ToUint64() - // We now know how many units we need. We take the asset ID from the - // RFQ so the recipient can match it back to the quote. - assetId, err := quote.Request.AssetSpecifier.UnwrapIdOrErr() - if err != nil { - return 0, nil, fmt.Errorf("quote has no asset ID: %w", err) + var assetId asset.ID + + switch { + case quote.Request.AssetSpecifier.HasId(): + assetId = *quote.Request.AssetSpecifier.UnwrapIdToPtr() + + case quote.Request.AssetSpecifier.HasGroupPubKey(): + // If a group key is defined in the quote we place the hash of + // the group key as the dummy asset ID in the HTLC. This asset + // balance in the HTLC is just a hint and the actual asset IDs + // will be picked later in the process. + groupKey := quote.Request.AssetSpecifier.UnwrapGroupKeyToPtr() + groupKeyX := schnorr.SerializePubKey(groupKey) + assetId = asset.ID(groupKeyX) } // If the number of asset units to send is zero due to integer division diff --git a/taprpc/marshal.go b/taprpc/marshal.go index 1fd2de722..0bc546078 100644 --- a/taprpc/marshal.go +++ b/taprpc/marshal.go @@ -15,10 +15,6 @@ import ( "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" - "github.com/lightninglabs/taproot-assets/rfq" - "github.com/lightninglabs/taproot-assets/rfqmath" - "github.com/lightninglabs/taproot-assets/rfqmsg" - "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntest" ) @@ -632,166 +628,3 @@ func MarshalAsset(ctx context.Context, a *asset.Asset, return rpcAsset, nil } - -// MarshalAcceptedSellQuoteEvent marshals a peer accepted sell quote event to -// its RPC representation. -func MarshalAcceptedSellQuoteEvent( - event *rfq.PeerAcceptedSellQuoteEvent) *rfqrpc.PeerAcceptedSellQuote { - - return MarshalAcceptedSellQuote(event.SellAccept) -} - -// MarshalAcceptedSellQuote marshals a peer accepted sell quote to its RPC -// representation. -func MarshalAcceptedSellQuote( - accept rfqmsg.SellAccept) *rfqrpc.PeerAcceptedSellQuote { - - rpcAssetRate := &rfqrpc.FixedPoint{ - Coefficient: accept.AssetRate.Rate.Coefficient.String(), - Scale: uint32(accept.AssetRate.Rate.Scale), - } - - // Calculate the equivalent asset units for the given total BTC amount - // based on the asset-to-BTC conversion rate. - numAssetUnits := rfqmath.MilliSatoshiToUnits( - accept.Request.PaymentMaxAmt, accept.AssetRate.Rate, - ) - - minTransportableMSat := rfqmath.MinTransportableMSat( - rfqmath.DefaultOnChainHtlcMSat, accept.AssetRate.Rate, - ) - - return &rfqrpc.PeerAcceptedSellQuote{ - Peer: accept.Peer.String(), - Id: accept.ID[:], - Scid: uint64(accept.ShortChannelId()), - BidAssetRate: rpcAssetRate, - Expiry: uint64(accept.AssetRate.Expiry.Unix()), - AssetAmount: numAssetUnits.ScaleTo(0).ToUint64(), - MinTransportableMsat: uint64(minTransportableMSat), - } -} - -// MarshalAcceptedBuyQuoteEvent marshals a peer accepted buy quote event to -// its rpc representation. -func MarshalAcceptedBuyQuoteEvent( - event *rfq.PeerAcceptedBuyQuoteEvent) (*rfqrpc.PeerAcceptedBuyQuote, - error) { - - // We now calculate the minimum amount of asset units that can be - // transported within a single HTLC for this asset at the given rate. - // This corresponds to the 354 satoshi minimum non-dust HTLC value. - minTransportableUnits := rfqmath.MinTransportableUnits( - rfqmath.DefaultOnChainHtlcMSat, event.AssetRate.Rate, - ).ScaleTo(0).ToUint64() - - return &rfqrpc.PeerAcceptedBuyQuote{ - Peer: event.Peer.String(), - Id: event.ID[:], - Scid: uint64(event.ShortChannelId()), - AssetMaxAmount: event.Request.AssetMaxAmt, - AskAssetRate: &rfqrpc.FixedPoint{ - Coefficient: event.AssetRate.Rate.Coefficient.String(), - Scale: uint32(event.AssetRate.Rate.Scale), - }, - Expiry: uint64(event.AssetRate.Expiry.Unix()), - MinTransportableUnits: minTransportableUnits, - }, nil -} - -// MarshalInvalidQuoteRespEvent marshals an invalid quote response event to -// its rpc representation. -func MarshalInvalidQuoteRespEvent( - event *rfq.InvalidQuoteRespEvent) *rfqrpc.InvalidQuoteResponse { - - peer := event.QuoteResponse.MsgPeer() - id := event.QuoteResponse.MsgID() - - return &rfqrpc.InvalidQuoteResponse{ - Status: rfqrpc.QuoteRespStatus(event.Status), - Peer: peer.String(), - Id: id[:], - } -} - -// MarshalIncomingRejectQuoteEvent marshals an incoming reject quote event to -// its RPC representation. -func MarshalIncomingRejectQuoteEvent( - event *rfq.IncomingRejectQuoteEvent) *rfqrpc.RejectedQuoteResponse { - - return &rfqrpc.RejectedQuoteResponse{ - Peer: event.Peer.String(), - Id: event.ID.Val[:], - ErrorMessage: event.Err.Val.Msg, - ErrorCode: uint32(event.Err.Val.Code), - } -} - -// NewAddAssetBuyOrderResponse creates a new AddAssetBuyOrderResponse from -// the given RFQ event. -func NewAddAssetBuyOrderResponse( - event fn.Event) (*rfqrpc.AddAssetBuyOrderResponse, error) { - - resp := &rfqrpc.AddAssetBuyOrderResponse{} - - switch e := event.(type) { - case *rfq.PeerAcceptedBuyQuoteEvent: - acceptedQuote, err := MarshalAcceptedBuyQuoteEvent(e) - if err != nil { - return nil, err - } - - resp.Response = &rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote{ - AcceptedQuote: acceptedQuote, - } - return resp, nil - - case *rfq.InvalidQuoteRespEvent: - resp.Response = &rfqrpc.AddAssetBuyOrderResponse_InvalidQuote{ - InvalidQuote: MarshalInvalidQuoteRespEvent(e), - } - return resp, nil - - case *rfq.IncomingRejectQuoteEvent: - resp.Response = &rfqrpc.AddAssetBuyOrderResponse_RejectedQuote{ - RejectedQuote: MarshalIncomingRejectQuoteEvent(e), - } - return resp, nil - - default: - return nil, fmt.Errorf("unknown AddAssetBuyOrder event "+ - "type: %T", e) - } -} - -// NewAddAssetSellOrderResponse creates a new AddAssetSellOrderResponse from -// the given RFQ event. -func NewAddAssetSellOrderResponse( - event fn.Event) (*rfqrpc.AddAssetSellOrderResponse, error) { - - resp := &rfqrpc.AddAssetSellOrderResponse{} - - switch e := event.(type) { - case *rfq.PeerAcceptedSellQuoteEvent: - resp.Response = &rfqrpc.AddAssetSellOrderResponse_AcceptedQuote{ - AcceptedQuote: MarshalAcceptedSellQuoteEvent(e), - } - return resp, nil - - case *rfq.InvalidQuoteRespEvent: - resp.Response = &rfqrpc.AddAssetSellOrderResponse_InvalidQuote{ - InvalidQuote: MarshalInvalidQuoteRespEvent(e), - } - return resp, nil - - case *rfq.IncomingRejectQuoteEvent: - resp.Response = &rfqrpc.AddAssetSellOrderResponse_RejectedQuote{ - RejectedQuote: MarshalIncomingRejectQuoteEvent(e), - } - return resp, nil - - default: - return nil, fmt.Errorf("unknown AddAssetSellOrder event "+ - "type: %T", e) - } -} diff --git a/taprpc/tapchannelrpc/tapchannel.pb.go b/taprpc/tapchannelrpc/tapchannel.pb.go index be64f1542..813dbd457 100644 --- a/taprpc/tapchannelrpc/tapchannel.pb.go +++ b/taprpc/tapchannelrpc/tapchannel.pb.go @@ -385,6 +385,9 @@ type SendPaymentRequest struct { // are sent out to the network than the invoice amount plus routing fees // require to be paid. AllowOverpay bool `protobuf:"varint,6,opt,name=allow_overpay,json=allowOverpay,proto3" json:"allow_overpay,omitempty"` + // The group key which dictates which assets may be used for this payment. + // Mutually exclusive to asset_id. + GroupKey []byte `protobuf:"bytes,7,opt,name=group_key,json=groupKey,proto3" json:"group_key,omitempty"` } func (x *SendPaymentRequest) Reset() { @@ -461,6 +464,13 @@ func (x *SendPaymentRequest) GetAllowOverpay() bool { return false } +func (x *SendPaymentRequest) GetGroupKey() []byte { + if x != nil { + return x.GroupKey + } + return nil +} + type SendPaymentResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -621,6 +631,10 @@ type AddInvoiceRequest struct { // won't be settled automatically. Instead, users will need to use the // invoicesrpc.SettleInvoice call to manually settle the invoice. HodlInvoice *HodlInvoice `protobuf:"bytes,5,opt,name=hodl_invoice,json=hodlInvoice,proto3" json:"hodl_invoice,omitempty"` + // The group key which dictates which assets may be accepted for this + // invoice. If set, any asset that belongs to this group may be accepted to + // settle this invoice. Mutually exclusive to asset_id. + GroupKey []byte `protobuf:"bytes,6,opt,name=group_key,json=groupKey,proto3" json:"group_key,omitempty"` } func (x *AddInvoiceRequest) Reset() { @@ -690,6 +704,13 @@ func (x *AddInvoiceRequest) GetHodlInvoice() *HodlInvoice { return nil } +func (x *AddInvoiceRequest) GetGroupKey() []byte { + if x != nil { + return x.GroupKey + } + return nil +} + type AddInvoiceResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -951,7 +972,7 @@ var file_tapchannelrpc_tapchannel_proto_rawDesc = []byte{ 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf7, 0x01, 0x0a, 0x12, 0x53, 0x65, 0x6e, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x94, 0x02, 0x0a, 0x12, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x73, @@ -967,102 +988,106 @@ var file_tapchannelrpc_tapchannel_proto_rawDesc = []byte{ 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x72, 0x66, 0x71, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x70, 0x61, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4f, 0x76, 0x65, 0x72, 0x70, - 0x61, 0x79, 0x22, 0xa9, 0x01, 0x0a, 0x13, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x13, 0x61, 0x63, - 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x6c, 0x5f, 0x6f, 0x72, 0x64, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, - 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x6c, - 0x6c, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x48, 0x00, 0x52, 0x11, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, - 0x65, 0x64, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x37, 0x0a, 0x0e, 0x70, - 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x30, - 0x0a, 0x0b, 0x48, 0x6f, 0x64, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x21, 0x0a, - 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, - 0x22, 0xea, 0x01, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, - 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, - 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, - 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x0f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, - 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x0e, - 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3d, - 0x0a, 0x0c, 0x68, 0x6f, 0x64, 0x6c, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x6f, 0x64, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, - 0x52, 0x0b, 0x68, 0x6f, 0x64, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x22, 0xa2, 0x01, - 0x0a, 0x12, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, - 0x5f, 0x62, 0x75, 0x79, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, - 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x10, - 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, - 0x12, 0x40, 0x0a, 0x0e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x52, 0x0d, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x22, 0x4e, 0x0a, 0x0b, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, - 0x71, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, - 0x70, 0x61, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x61, 0x79, 0x52, 0x65, 0x71, 0x53, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x22, 0x8e, 0x02, 0x0a, 0x13, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, - 0x65, 0x71, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x73, - 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3f, 0x0a, - 0x0f, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, - 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x52, 0x0e, - 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x12, 0x33, - 0x0a, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, - 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x12, 0x36, 0x0a, 0x0c, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x5f, 0x69, - 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x61, 0x70, 0x72, - 0x70, 0x63, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, - 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x26, 0x0a, 0x07, 0x70, - 0x61, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x52, 0x06, 0x70, 0x61, 0x79, - 0x52, 0x65, 0x71, 0x32, 0xda, 0x03, 0x0a, 0x14, 0x54, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x54, 0x0a, 0x0b, - 0x46, 0x75, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x21, 0x2e, 0x74, 0x61, - 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, - 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, - 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x46, - 0x75, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, - 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x29, 0x2e, 0x74, 0x61, 0x70, 0x63, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x65, - 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, - 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x56, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, - 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, - 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x51, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x49, - 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x20, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, - 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, - 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x11, 0x44, - 0x65, 0x63, 0x6f, 0x64, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, - 0x12, 0x1a, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, - 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x22, 0x2e, 0x74, - 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, - 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, - 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, - 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, - 0x70, 0x63, 0x2f, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x22, + 0xa9, 0x01, 0x0a, 0x13, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x13, 0x61, 0x63, 0x63, 0x65, 0x70, + 0x74, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x6c, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, + 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x6c, 0x6c, 0x51, 0x75, + 0x6f, 0x74, 0x65, 0x48, 0x00, 0x52, 0x11, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x53, + 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x37, 0x0a, 0x0e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x48, 0x00, 0x52, 0x0d, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x30, 0x0a, 0x0b, 0x48, + 0x6f, 0x64, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x22, 0x87, 0x02, + 0x0a, 0x11, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x21, + 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6b, + 0x65, 0x79, 0x12, 0x37, 0x0a, 0x0f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x0e, 0x69, 0x6e, 0x76, + 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x68, + 0x6f, 0x64, 0x6c, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, + 0x63, 0x2e, 0x48, 0x6f, 0x64, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x0b, 0x68, + 0x6f, 0x64, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x22, 0xa2, 0x01, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x49, + 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, + 0x0a, 0x12, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x75, 0x79, 0x5f, 0x71, + 0x75, 0x6f, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x66, 0x71, + 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, + 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x10, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, + 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x40, 0x0a, 0x0e, 0x69, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0d, 0x69, + 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x4e, 0x0a, 0x0b, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x19, 0x0a, 0x08, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x61, 0x79, 0x5f, 0x72, 0x65, + 0x71, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x70, 0x61, 0x79, 0x52, 0x65, 0x71, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x8e, 0x02, 0x0a, + 0x13, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, + 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x64, 0x65, 0x63, 0x69, 0x6d, + 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, + 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x52, 0x0e, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, + 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x12, 0x33, 0x0a, 0x0b, 0x61, 0x73, 0x73, 0x65, + 0x74, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x52, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x36, 0x0a, + 0x0c, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x6e, + 0x65, 0x73, 0x69, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, + 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x26, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x5f, 0x72, 0x65, 0x71, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, + 0x61, 0x79, 0x52, 0x65, 0x71, 0x52, 0x06, 0x70, 0x61, 0x79, 0x52, 0x65, 0x71, 0x32, 0xda, 0x03, + 0x0a, 0x14, 0x54, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x54, 0x0a, 0x0b, 0x46, 0x75, 0x6e, 0x64, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x43, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, + 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x73, 0x12, 0x29, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x72, 0x70, 0x63, 0x2e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, + 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x45, + 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x0b, 0x53, 0x65, + 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, + 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, + 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x30, 0x01, 0x12, 0x51, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x12, 0x20, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, + 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, + 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x11, 0x44, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x1a, 0x2e, 0x74, 0x61, 0x70, + 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, + 0x65, 0x71, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, + 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2f, 0x74, 0x61, 0x70, + 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/taprpc/tapchannelrpc/tapchannel.proto b/taprpc/tapchannelrpc/tapchannel.proto index 546d12db9..cfc96d23c 100644 --- a/taprpc/tapchannelrpc/tapchannel.proto +++ b/taprpc/tapchannelrpc/tapchannel.proto @@ -37,7 +37,9 @@ service TaprootAssetChannels { /* AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset specific parameters. It allows RPC users to create invoices that correspond - to the specified asset amount. + to the specified asset amount. If a peer pubkey is specified, then only that + peer will be used for RFQ negotiations. If none is specified then RFQ quotes + for all peers with suitable asset channels will be created. */ rpc AddInvoice (AddInvoiceRequest) returns (AddInvoiceResponse); @@ -142,6 +144,10 @@ message SendPaymentRequest { // are sent out to the network than the invoice amount plus routing fees // require to be paid. bool allow_overpay = 6; + + // The group key which dictates which assets may be used for this payment. + // Mutually exclusive to asset_id. + bytes group_key = 7; } message SendPaymentResponse { @@ -188,6 +194,11 @@ message AddInvoiceRequest { // won't be settled automatically. Instead, users will need to use the // invoicesrpc.SettleInvoice call to manually settle the invoice. HodlInvoice hodl_invoice = 5; + + // The group key which dictates which assets may be accepted for this + // invoice. If set, any asset that belongs to this group may be accepted to + // settle this invoice. Mutually exclusive to asset_id. + bytes group_key = 6; } message AddInvoiceResponse { diff --git a/taprpc/tapchannelrpc/tapchannel.swagger.json b/taprpc/tapchannelrpc/tapchannel.swagger.json index 957365537..bd97c5aa6 100644 --- a/taprpc/tapchannelrpc/tapchannel.swagger.json +++ b/taprpc/tapchannelrpc/tapchannel.swagger.json @@ -84,7 +84,7 @@ }, "/v1/taproot-assets/channels/invoice": { "post": { - "summary": "AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset\nspecific parameters. It allows RPC users to create invoices that correspond\nto the specified asset amount.", + "summary": "AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset\nspecific parameters. It allows RPC users to create invoices that correspond\nto the specified asset amount. If a peer pubkey is specified, then only that\npeer will be used for RFQ negotiations. If none is specified then RFQ quotes\nfor all peers with suitable asset channels will be created.", "operationId": "TaprootAssetChannels_AddInvoice", "responses": { "200": { @@ -1512,6 +1512,11 @@ "hodl_invoice": { "$ref": "#/definitions/tapchannelrpcHodlInvoice", "description": "If set, then this will make the invoice created a hodl invoice, which\nwon't be settled automatically. Instead, users will need to use the\ninvoicesrpc.SettleInvoice call to manually settle the invoice." + }, + "group_key": { + "type": "string", + "format": "byte", + "description": "The group key which dictates which assets may be accepted for this\ninvoice. If set, any asset that belongs to this group may be accepted to\nsettle this invoice. Mutually exclusive to asset_id." } } }, @@ -1690,6 +1695,11 @@ "allow_overpay": { "type": "boolean", "description": "If a small invoice should be paid that is below the amount that always\nneeds to be sent out to carry a single asset unit, then by default the\npayment is rejected. If this flag is set, then the payment will be\nallowed to proceed, even if it is uneconomical, meaning that more sats\nare sent out to the network than the invoice amount plus routing fees\nrequire to be paid." + }, + "group_key": { + "type": "string", + "format": "byte", + "description": "The group key which dictates which assets may be used for this payment.\nMutually exclusive to asset_id." } } }, diff --git a/taprpc/tapchannelrpc/tapchannel_grpc.pb.go b/taprpc/tapchannelrpc/tapchannel_grpc.pb.go index 1da8f92e8..70d5fc8a9 100644 --- a/taprpc/tapchannelrpc/tapchannel_grpc.pb.go +++ b/taprpc/tapchannelrpc/tapchannel_grpc.pb.go @@ -34,7 +34,9 @@ type TaprootAssetChannelsClient interface { SendPayment(ctx context.Context, in *SendPaymentRequest, opts ...grpc.CallOption) (TaprootAssetChannels_SendPaymentClient, error) // AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset // specific parameters. It allows RPC users to create invoices that correspond - // to the specified asset amount. + // to the specified asset amount. If a peer pubkey is specified, then only that + // peer will be used for RFQ negotiations. If none is specified then RFQ quotes + // for all peers with suitable asset channels will be created. AddInvoice(ctx context.Context, in *AddInvoiceRequest, opts ...grpc.CallOption) (*AddInvoiceResponse, error) // DecodeAssetPayReq is similar to lnd's lnrpc.DecodePayReq, but it accepts an // asset ID and returns the invoice amount expressed in asset units along side @@ -138,7 +140,9 @@ type TaprootAssetChannelsServer interface { SendPayment(*SendPaymentRequest, TaprootAssetChannels_SendPaymentServer) error // AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset // specific parameters. It allows RPC users to create invoices that correspond - // to the specified asset amount. + // to the specified asset amount. If a peer pubkey is specified, then only that + // peer will be used for RFQ negotiations. If none is specified then RFQ quotes + // for all peers with suitable asset channels will be created. AddInvoice(context.Context, *AddInvoiceRequest) (*AddInvoiceResponse, error) // DecodeAssetPayReq is similar to lnd's lnrpc.DecodePayReq, but it accepts an // asset ID and returns the invoice amount expressed in asset units along side