diff --git a/asset/asset.go b/asset/asset.go index fdcb915185..994ad19106 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -348,6 +348,23 @@ func NewSpecifierFromGroupKey(groupPubKey btcec.PublicKey) Specifier { } } +// NewExlusiveSpecifier creates a specifier that may only include one of asset +// ID or group key. If both are set then a specifier over the group key is +// created. +func NewExclusiveSpecifier(id *ID, + groupPubKey *btcec.PublicKey) (Specifier, error) { + + switch { + case groupPubKey != nil: + return NewSpecifierFromGroupKey(*groupPubKey), nil + + case id != nil: + return NewSpecifierFromId(*id), nil + } + + return Specifier{}, fmt.Errorf("must set either asset ID or group key") +} + // String returns a human-readable description of the specifier. func (s *Specifier) String() string { // An unset asset ID is represented as an empty string. diff --git a/rfq/manager.go b/rfq/manager.go index 96da8e7811..73fd336f22 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -4,12 +4,15 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "fmt" "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" @@ -232,6 +235,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 +952,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 +979,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 X coordinate + // 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 +1000,6 @@ func (m *Manager) AssetMatchesSpecifier(ctx context.Context, return false, nil } - specifierGK := specifier.UnwrapGroupKeyToPtr() - return group.UnwrapToPtr().IsEqual(specifierGK), nil case specifier.HasId(): diff --git a/rfq/marshal.go b/rfq/marshal.go new file mode 100644 index 0000000000..6c8cd10303 --- /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 3dcf1a331d..2b5c58f156 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 596f3659be..b896f6ad69 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 69c68bcc12..9898fb013e 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 f81ff85c69..733f8fa2f4 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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) @@ -6803,7 +6828,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 +7016,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 +7032,7 @@ func marshallRfqEvent(eventInterface fn.Event) (*rfqrpc.RfqEvent, error) { }, nil case *rfq.PeerAcceptedSellQuoteEvent: - rpcAcceptedQuote := taprpc.MarshalAcceptedSellQuoteEvent( + rpcAcceptedQuote := rfq.MarshalAcceptedSellQuoteEvent( event, ) @@ -7221,19 +7246,27 @@ 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") + assetID, groupKey, err := parseAssetSpecifier( + req.AssetId, "", req.GroupKey, "", + ) + if err != nil { + return err + } + + specifier, err := asset.NewExclusiveSpecifier(assetID, groupKey) + if err != nil { + return err } - 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 +7329,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,7 +7391,7 @@ 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( @@ -7384,14 +7417,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 +7503,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 +7679,26 @@ 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") + assetID, groupKey, err := parseAssetSpecifier( + req.AssetId, "", req.GroupKey, "", + ) + if err != nil { + return nil, err + } + + specifier, err := asset.NewExclusiveSpecifier(assetID, groupKey) + if err != nil { + return nil, err } - var assetID asset.ID - copy(assetID[:], req.AssetId) // The peer public key is optional if there is only a single asset // channel. @@ -7653,8 +7713,6 @@ 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, @@ -7676,15 +7734,13 @@ func (r *rpcServer) AddInvoice(ctx context.Context, time.Duration(expirySeconds) * time.Second, ) + rpcSpecifier := marshalAssetSpecifier(specifier) + 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[:], + AssetSpecifier: &rpcSpecifier, + AssetMaxAmt: req.AssetAmount, + Expiry: uint64(expiryTimestamp.Unix()), + PeerPubKey: peerPubKey[:], TimeoutSeconds: uint32( rfq.DefaultTimeout.Seconds(), ), diff --git a/tapchannel/aux_invoice_manager.go b/tapchannel/aux_invoice_manager.go index 511804233d..92be86be7b 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 e180ca17b8..e04132bc61 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 ef3fe93b87..286b292c09 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 X + // coordinate 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 1fd2de7226..0bc5460788 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 be64f1542a..00cec6bf08 100644 --- a/taprpc/tapchannelrpc/tapchannel.pb.go +++ b/taprpc/tapchannelrpc/tapchannel.pb.go @@ -354,7 +354,7 @@ type SendPaymentRequest struct { // The asset ID to use for the payment. This must be set for both invoice // and keysend payments, unless RFQ negotiation was already done beforehand // and payment_request.first_hop_custom_records already contains valid RFQ - // data. + // data. Mutually exclusive to group_key. AssetId []byte `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // The asset amount to send in a keysend payment. This amount is ignored for // invoice payments as the asset amount is negotiated through RFQ with the @@ -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 @@ -601,7 +611,7 @@ type AddInvoiceRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The asset ID to use for the invoice. + // The asset ID to use for the invoice. Mutually exclusive to group_key. AssetId []byte `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // The asset amount to receive. AssetAmount uint64 `protobuf:"varint,2,opt,name=asset_amount,json=assetAmount,proto3" json:"asset_amount,omitempty"` @@ -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 546d12db9f..b76aa77857 100644 --- a/taprpc/tapchannelrpc/tapchannel.proto +++ b/taprpc/tapchannelrpc/tapchannel.proto @@ -106,7 +106,7 @@ message SendPaymentRequest { // The asset ID to use for the payment. This must be set for both invoice // and keysend payments, unless RFQ negotiation was already done beforehand // and payment_request.first_hop_custom_records already contains valid RFQ - // data. + // data. Mutually exclusive to group_key. bytes asset_id = 1; // The asset amount to send in a keysend payment. This amount is ignored for @@ -142,6 +142,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 { @@ -164,7 +168,7 @@ message HodlInvoice { } message AddInvoiceRequest { - // The asset ID to use for the invoice. + // The asset ID to use for the invoice. Mutually exclusive to group_key. bytes asset_id = 1; // The asset amount to receive. @@ -188,6 +192,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 9573655374..3b9a6b3b92 100644 --- a/taprpc/tapchannelrpc/tapchannel.swagger.json +++ b/taprpc/tapchannelrpc/tapchannel.swagger.json @@ -1493,7 +1493,7 @@ "asset_id": { "type": "string", "format": "byte", - "description": "The asset ID to use for the invoice." + "description": "The asset ID to use for the invoice. Mutually exclusive to group_key." }, "asset_amount": { "type": "string", @@ -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." } } }, @@ -1666,7 +1671,7 @@ "asset_id": { "type": "string", "format": "byte", - "description": "The asset ID to use for the payment. This must be set for both invoice\nand keysend payments, unless RFQ negotiation was already done beforehand\nand payment_request.first_hop_custom_records already contains valid RFQ\ndata." + "description": "The asset ID to use for the payment. This must be set for both invoice\nand keysend payments, unless RFQ negotiation was already done beforehand\nand payment_request.first_hop_custom_records already contains valid RFQ\ndata. Mutually exclusive to group_key." }, "asset_amount": { "type": "string", @@ -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." } } },