diff --git a/rfq/manager.go b/rfq/manager.go index 125b0230f..b1c124a8a 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -6,6 +6,8 @@ import ( "encoding/json" "errors" "fmt" + "sort" + "strings" "sync" "time" @@ -17,10 +19,12 @@ import ( "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmsg" - lfn "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + "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 ( @@ -41,7 +45,9 @@ const ( type ChannelLister interface { // ListChannels returns a list of channels that are available for // routing. - ListChannels(ctx context.Context) ([]lndclient.ChannelInfo, error) + ListChannels(ctx context.Context, activeOnly, publicOnly bool, + _ ...lndclient.ListChannelsOption) ([]lndclient.ChannelInfo, + error) } // ScidAliasManager is an interface that can add short channel ID (SCID) aliases @@ -546,63 +552,38 @@ func (m *Manager) handleOutgoingMessage(outgoingMsg rfqmsg.OutgoingMsg) error { func (m *Manager) addScidAlias(scidAlias uint64, assetSpecifier asset.Specifier, peer route.Vertex) error { - // Retrieve all local channels. ctxb := context.Background() - localChans, err := m.cfg.ChannelLister.ListChannels(ctxb) - if err != nil { - // Not being able to call lnd to add the alias is a critical - // error, which warrants shutting down, as something is wrong. - return fn.NewCriticalError( - fmt.Errorf("add alias: error listing local channels: "+ - "%w", err), - ) - } - - // Filter for channels with the given peer. - peerChannels := lfn.Filter( - localChans, func(c lndclient.ChannelInfo) bool { - return c.PubKeyBytes == peer - }, + peerChans, err := m.FetchChannel( + ctxb, assetSpecifier, &peer, NoIntention, ) + if err != nil && !strings.Contains( + err.Error(), "no asset channel balance found for", + ) { - var baseSCID uint64 - for _, localChan := range peerChannels { - if len(localChan.CustomChannelData) == 0 { - continue - } - - var assetData rfqmsg.JsonAssetChannel - err = json.Unmarshal(localChan.CustomChannelData, &assetData) - if err != nil { - log.Warnf("Unable to unmarshal channel asset data: %v", - err) - continue - } + return err + } - match, err := m.ChannelMatchesFully( - ctxb, assetData, assetSpecifier, + // As a fallback, if we didn't find any compatible channels with the + // peer, let's pick any channel that is available with this peer. This + // is okay, because non-strict forwarding will ask each channel if the + // bandwidth matches the provided specifier. + if len(peerChans[peer]) == 0 { + peerChans, err = m.FetchChannel( + ctxb, asset.Specifier{}, &peer, NoIntention, ) if err != nil { return err } - - // TODO(george): Instead of returning the first result, - // try to pick the best channel for what we're trying to - // do (receive/send). Binding a baseSCID means we're - // also binding the asset liquidity on that channel. - if match { - baseSCID = localChan.ChannelID - break - } } - // As a fallback, if the base SCID is not found and there's only one - // channel with the target peer, assume that the base SCID corresponds - // to that channel. - if baseSCID == 0 && len(peerChannels) == 1 { - baseSCID = peerChannels[0].ChannelID + if len(peerChans[peer]) == 0 { + return fmt.Errorf("cannot add scid alias with peer=%v, no "+ + "compatible channels found for %s", peer, + &assetSpecifier) } + baseSCID := peerChans[peer][0].ChannelInfo.ChannelID + // At this point, if the base SCID is still not found, we return an // error. We can't map the SCID alias to a base SCID. if baseSCID == 0 { @@ -1052,6 +1033,250 @@ func (m *Manager) ChannelMatchesFully(ctx context.Context, return true, nil } +// TapChannel is a helper struct that combines the information of an asset +// specifier that is satisfied by a channel with the channels' general +// information. +type TapChannel 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.JsonAssetChannel +} + +// PeerChanMap is a structure that maps peers to channels. This is used for +// filtering asset channels against an asset specifier. +type PeerChanMap map[route.Vertex][]TapChannel + +// 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) (PeerChanMap, bool, error) { + + var ( + peerChanMap = make(PeerChanMap) + haveGroupedAssetChannels bool + ) + for chanIdx := range activeChannels { + var ( + pass bool + assetData rfqmsg.JsonAssetChannel + ) + + openChan := activeChannels[chanIdx] + + // If there specifier is empty, we skip all asset balance + // related checks. + if specifier.IsSome() { + if len(openChan.CustomChannelData) == 0 { + continue + } + + err := json.Unmarshal( + openChan.CustomChannelData, &assetData, + ) + if err != nil { + return nil, false, fmt.Errorf("unable to "+ + "unmarshal asset data: %w", err) + } + + if len(assetData.GroupKey) > 0 { + haveGroupedAssetChannels = true + } + + // Check if the assets of this channel match the + // provided specifier. + pass, err = m.ChannelMatchesFully( + ctx, assetData, specifier, + ) + if err != nil { + return nil, false, err + } + } + + // We also append the channel in the case where the specifier is + // empty. This means that the caller doesn't really care about + // the type of balance. + if pass || !specifier.IsSome() { + peerChanMap[openChan.PubKeyBytes] = append( + peerChanMap[openChan.PubKeyBytes], + TapChannel{ + Specifier: specifier, + ChannelInfo: openChan, + AssetInfo: assetData, + }, + ) + } + } + + return peerChanMap, haveGroupedAssetChannels, nil +} + +// 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 +) + +// FetchChannel 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) FetchChannel(ctx context.Context, specifier asset.Specifier, + peerPubKey *route.Vertex, + intention ChanIntention) (PeerChanMap, error) { + + activeChannels, err := m.cfg.ChannelLister.ListChannels( + ctx, true, false, + ) + if err != nil { + return nil, err + } + + balancesMap, haveGroupChans, err := m.ComputeChannelAssetBalance( + ctx, activeChannels, specifier, + ) + if err != nil { + return nil, fmt.Errorf("error computing available asset "+ + "channel balance: %w", err) + } + + // If the user uses the asset ID to specify what asset to use, that will + // not work for asset channels that have multiple UTXOs of grouped + // assets. The result wouldn't be deterministic anyway (meaning, it's + // not guaranteed that a specific asset ID is moved within a channel + // when an HTLC is sent, the allocation logic decides which actual UTXO + // is used). So we tell the user to use the group key instead, at least + // for channels that have multiple UTXOs of grouped assets. + if specifier.HasId() && len(balancesMap) == 0 && haveGroupChans { + return nil, fmt.Errorf("no compatible asset channel found for "+ + "%s, make sure to use group key for grouped asset "+ + "channels", &specifier) + } + + 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 k, v := range balancesMap { + sort.Slice(v, func(i, j int) bool { + return v[i].AssetInfo.LocalBalance > + v[j].AssetInfo.LocalBalance + }) + + balancesMap[k] = v + } + case ReceiveIntention: + // When sending we care about the volume of the remote balances, + // so we sort by remote balances in descending order. + for k, v := range balancesMap { + sort.Slice(v, func(i, j int) bool { + return v[i].AssetInfo.RemoteBalance > + v[j].AssetInfo.RemoteBalance + }) + + balancesMap[k] = v + } + 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) + } + + filteredRes := make(PeerChanMap) + filteredRes[*peerPubKey] = balancesMap[*peerPubKey] + + balancesMap = filteredRes + } + + 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. +// Depending on whether the hold flag is set, we either return the lnrpc version +// of a hop hint or the zpay32 version. This is because we use the lndclient +// wrapper for hold invoices while we use the raw lnrpc endpoint for simple +// invoices. +func (m *Manager) RfqToHopHint(ctx context.Context, + policyFetcher InboundPolicyFetcher, channelID uint64, + peerPubKey route.Vertex, quote *rfqrpc.PeerAcceptedBuyQuote, + hold bool) (*zpay32.HopHint, error) { + + inboundPolicy, err := policyFetcher(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) + } + + peerPub, err := btcec.ParsePubKey(peerPubKey[:]) + if err != nil { + return 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 hopHint, nil +} + +// Zpay32HopHintToLnrpc converts a zpay32 hop hint to the lnrpc representation. +func Zpay32HopHintToLnrpc(h *zpay32.HopHint) *lnrpc.HopHint { + return &lnrpc.HopHint{ + NodeId: fmt.Sprintf( + "%x", h.NodeID.SerializeCompressed(), + ), + ChanId: h.ChannelID, + FeeBaseMsat: h.FeeBaseMSat, + FeeProportionalMillionths: h.FeeProportionalMillionths, + CltvExpiryDelta: uint32(h.CLTVExpiryDelta), + } +} + // 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/manager_test.go b/rfq/manager_test.go new file mode 100644 index 000000000..5e0452b00 --- /dev/null +++ b/rfq/manager_test.go @@ -0,0 +1,352 @@ +package rfq + +import ( + "context" + "encoding/binary" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/proof" + tpchmsg "github.com/lightninglabs/taproot-assets/tapchannelmsg" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +var ( + asset1 = asset.Asset{ + ScriptKey: asset.ScriptKey{ + PubKey: asset.NUMSPubKey, + }, + Genesis: asset.Genesis{ + Tag: "111", + }, + } + asset2 = asset.Asset{ + ScriptKey: asset.ScriptKey{ + PubKey: asset.NUMSPubKey, + }, + Genesis: asset.Genesis{ + Tag: "222", + }, + } + asset3 = asset.Asset{ + ScriptKey: asset.ScriptKey{ + PubKey: asset.NUMSPubKey, + }, + Genesis: asset.Genesis{ + Tag: "333", + }, + } + testAssetID1 = asset1.ID() + testAssetID2 = asset2.ID() + testAssetID3 = asset3.ID() + proof1 = proof.Proof{ + Asset: asset1, + } + proof2 = proof.Proof{ + Asset: asset2, + } + proof3 = proof.Proof{ + Asset: asset3, + } + testGroupKey = pubKeyFromUint64(2121) + peer1 = route.Vertex{88} + peer2 = route.Vertex{77} +) + +// GroupLookupMock mocks the GroupLookup interface that is required by the +// rfq manager to check asset IDs against asset specifiers. +type GroupLookupMock struct{} + +// QueryAssetGroup fetches the group information of an asset, if it belongs in a +// group. +func (g *GroupLookupMock) QueryAssetGroup(_ context.Context, + id asset.ID) (*asset.AssetGroup, error) { + + // We only consider testAssetID1 and testAssetID2 to be in the group. + if id == testAssetID1 || id == testAssetID2 { + return &asset.AssetGroup{ + GroupKey: &asset.GroupKey{ + GroupPubKey: *testGroupKey, + }, + }, nil + } + + return nil, address.ErrAssetGroupUnknown +} + +// testCaseComputeChannelAssetBalance is a test case for computing the channel +// asset balances. +type testCaseComputeChannelAssetBalance struct { + name string + activeChannels []lndclient.ChannelInfo + specifier asset.Specifier + expectedValidPeers int + expectedLocalBal uint64 + expectedRemoteBal uint64 +} + +// createChannelWithCustomData creates a dummy channel with only the custom data +// and peer fields populated. The custom data encode the local and remote +// balances of the given asset ID. +func createChannelWithCustomData(t *testing.T, id asset.ID, localBalance, + remoteBalance uint64, proof proof.Proof, + peer route.Vertex) lndclient.ChannelInfo { + + customData := tpchmsg.ChannelCustomData{ + LocalCommit: *tpchmsg.NewCommitment( + []*tpchmsg.AssetOutput{ + tpchmsg.NewAssetOutput( + id, localBalance, proof, + ), + }, + []*tpchmsg.AssetOutput{ + tpchmsg.NewAssetOutput( + id, remoteBalance, proof, + ), + }, + nil, nil, lnwallet.CommitAuxLeaves{}, + ), + OpenChan: *tpchmsg.NewOpenChannel( + []*tpchmsg.AssetOutput{ + tpchmsg.NewAssetOutput( + id, + localBalance+remoteBalance, proof, + ), + }, 0, nil, + ), + } + + data, err := customData.AsJson() + require.NoError(t, err) + + return lndclient.ChannelInfo{ + CustomChannelData: data, + PubKeyBytes: peer, + } +} + +// assertComputeChannelAssetBalance asserts that the manager can compute the +// correct asset balances for the test case. It also compares the results +// against some expected values. +func assertComputeChannelAssetBalance(t *testing.T, + tc testCaseComputeChannelAssetBalance) { + + mockGroupLookup := &GroupLookupMock{} + cfg := ManagerCfg{ + GroupLookup: mockGroupLookup, + } + manager, err := NewManager(cfg) + require.NoError(t, err) + + ctxt, cancel := context.WithTimeout( + context.Background(), DefaultTimeout, + ) + defer cancel() + + chanMap, _, err := manager.ComputeChannelAssetBalance( + ctxt, tc.activeChannels, tc.specifier, + ) + require.NoError(t, err) + + // We avoid using require.Len directly on the map here as it will print + // the whole map on fail. + require.Equal(t, tc.expectedValidPeers, len(chanMap)) + + var totalLocal, totalRemote uint64 + + for _, v := range chanMap { + for _, ch := range v { + totalLocal += ch.AssetInfo.LocalBalance + totalRemote += ch.AssetInfo.RemoteBalance + } + } + + require.Equal(t, tc.expectedLocalBal, totalLocal) + require.Equal(t, tc.expectedRemoteBal, totalRemote) +} + +// TestComputeChannelAssetBalance tests that the rfq manager can correctly +// filter the channels according to the asset ID of the channel and the provided +// asset specifier. +func TestComputeChannelAssetBlanace(t *testing.T) { + testCases := []testCaseComputeChannelAssetBalance{ + { + name: "1 asset 1 channel 1 peer", + activeChannels: []lndclient.ChannelInfo{ + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer1, + ), + }, + specifier: asset.NewSpecifierFromId( + testAssetID1, + ), + expectedValidPeers: 1, + expectedLocalBal: 10_000, + expectedRemoteBal: 15_000, + }, + { + name: "1 asset 2 channels 1 peer", + activeChannels: []lndclient.ChannelInfo{ + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer1, + ), + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer1, + ), + }, + specifier: asset.NewSpecifierFromId( + testAssetID1, + ), + expectedValidPeers: 1, + expectedLocalBal: 20_000, + expectedRemoteBal: 30_000, + }, + { + name: "1 asset 2 channels 2 peers", + activeChannels: []lndclient.ChannelInfo{ + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer1, + ), + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer2, + ), + }, + specifier: asset.NewSpecifierFromId( + testAssetID1, + ), + expectedValidPeers: 2, + expectedLocalBal: 20_000, + expectedRemoteBal: 30_000, + }, + { + name: "2 assets 2 channels 2 peers, asset specifier", + activeChannels: []lndclient.ChannelInfo{ + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer1, + ), + createChannelWithCustomData( + t, testAssetID2, 10_000, 15_000, proof2, + peer2, + ), + }, + specifier: asset.NewSpecifierFromId( + testAssetID1, + ), + expectedValidPeers: 1, + expectedLocalBal: 10_000, + expectedRemoteBal: 15_000, + }, + { + name: "2 assets 2 channels 2 peers, group specifier", + activeChannels: []lndclient.ChannelInfo{ + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer1, + ), + createChannelWithCustomData( + t, testAssetID2, 10_000, 15_000, proof2, + peer2, + ), + }, + specifier: asset.NewSpecifierFromGroupKey( + *testGroupKey, + ), + expectedValidPeers: 2, + expectedLocalBal: 20_000, + expectedRemoteBal: 30_000, + }, + { + name: "3 assets 3 channels 2 peers, group specifier", + activeChannels: []lndclient.ChannelInfo{ + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer1, + ), + createChannelWithCustomData( + t, testAssetID2, 10_000, 15_000, proof2, + peer2, + ), + createChannelWithCustomData( + t, testAssetID3, 10_000, 15_000, proof3, + peer2, + ), + }, + specifier: asset.NewSpecifierFromGroupKey( + *testGroupKey, + ), + expectedValidPeers: 2, + expectedLocalBal: 20_000, + expectedRemoteBal: 30_000, + }, + { + name: "3 assets 6 channels 2 peers, group specifier", + activeChannels: []lndclient.ChannelInfo{ + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer1, + ), + createChannelWithCustomData( + t, testAssetID2, 10_000, 15_000, proof2, + peer1, + ), + createChannelWithCustomData( + t, testAssetID3, 10_000, 15_000, proof3, + peer1, + ), + createChannelWithCustomData( + t, testAssetID1, 10_000, 15_000, proof1, + peer2, + ), + createChannelWithCustomData( + t, testAssetID2, 10_000, 15_000, proof2, + peer2, + ), + createChannelWithCustomData( + t, testAssetID3, 10_000, 15_000, proof3, + peer2, + ), + }, + specifier: asset.NewSpecifierFromGroupKey( + *testGroupKey, + ), + expectedValidPeers: 2, + expectedLocalBal: 40_000, + expectedRemoteBal: 60_000, + }, + } + + for idx := range testCases { + tc := testCases[idx] + + success := t.Run(tc.name, func(t *testing.T) { + assertComputeChannelAssetBalance(t, tc) + }) + if !success { + break + } + } +} + +// pubKeyFromUint64 is a helper function that generates a public key from a +// uint64 value. +func pubKeyFromUint64(num uint64) *btcec.PublicKey { + var ( + buf = make([]byte, 8) + scalar = new(secp256k1.ModNScalar) + ) + binary.BigEndian.PutUint64(buf, num) + _ = scalar.SetByteSlice(buf) + return secp256k1.NewPrivateKey(scalar).PubKey() +} diff --git a/rpcserver.go b/rpcserver.go index 44985de14..6de277fdc 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" @@ -48,6 +48,7 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc" @@ -103,6 +104,10 @@ const ( // proofTypeReceive is an alias for the proof type used for receiving // assets. proofTypeReceive = tapdevrpc.ProofTransferType_PROOF_TRANSFER_TYPE_RECEIVE + + // maxRfqHopHints is the maximum number of RFQ quotes that may be + // encoded as hop hints in a bolt11 invoice. + maxRfqHopHints = 20 ) type ( @@ -6929,7 +6934,9 @@ 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.FetchChannel( + ctx, specifier, &peer, rfq.NoIntention, + ) if err != nil { return fmt.Errorf("error checking asset channel: %w", err) @@ -7634,18 +7641,31 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest, 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.FetchChannel( + ctx, 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 @@ -7756,7 +7776,9 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest, // We check that we have the asset amount available in the // channel. - _, err = r.rfqChannel(ctx, specifier, &dest, SendIntention) + _, err = r.cfg.RfqManager.FetchChannel( + ctx, specifier, &dest, rfq.SendIntention, + ) if err != nil { return fmt.Errorf("error finding asset channel to "+ "use: %w", err) @@ -7952,6 +7974,15 @@ func (r *rpcServer) AddInvoice(ctx context.Context, return nil, fmt.Errorf("invoice request must be specified") } iReq := req.InvoiceRequest + existingQuotes := iReq.RouteHints != nil + + if existingQuotes && !tapchannel.IsAssetInvoice( + iReq, r.cfg.AuxInvoiceManager, + ) { + + return nil, fmt.Errorf("existing route hints should only " + + "contain valid accepted quotes") + } assetID, groupKey, err := parseAssetSpecifier( req.AssetId, "", req.GroupKey, "", @@ -7979,18 +8010,14 @@ func (r *rpcServer) AddInvoice(ctx context.Context, } // We can now query the asset channels we have. - assetChan, err := r.rfqChannel( - ctx, specifier, peerPubKey, ReceiveIntention, + chanMap, err := r.cfg.RfqManager.FetchChannel( + ctx, 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()) @@ -8014,43 +8041,86 @@ func (r *rpcServer) AddInvoice(ctx context.Context, rpcSpecifier := marshalAssetSpecifier(specifier) - resp, err := r.AddAssetBuyOrder(ctx, &rfqrpc.AddAssetBuyOrderRequest{ - AssetSpecifier: &rpcSpecifier, - AssetMaxAmt: maxUnits, - Expiry: uint64(expiryTimestamp.Unix()), - PeerPubKey: peerPubKey[:], - TimeoutSeconds: uint32( - rfq.DefaultTimeout.Seconds(), - ), - }) - if err != nil { - return nil, fmt.Errorf("error adding buy order: %w", err) + type quoteWithInfo struct { + quote *rfqrpc.PeerAcceptedBuyQuote + rate *rfqmath.BigIntFixedPoint + channel rfq.TapChannel } - var acceptedQuote *rfqrpc.PeerAcceptedBuyQuote - switch r := resp.Response.(type) { - case *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote: - acceptedQuote = r.AcceptedQuote + var acquiredQuotes []quoteWithInfo - case *rfqrpc.AddAssetBuyOrderResponse_InvalidQuote: - return nil, fmt.Errorf("peer %v sent back an invalid quote, "+ - "status: %v", r.InvalidQuote.Peer, - r.InvalidQuote.Status.String()) + for peer, channels := range chanMap { + if existingQuotes { + break + } - 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) + quote, err := r.acquireBuyOrder( + ctx, &rpcSpecifier, maxUnits, expiryTimestamp, + &peer, + ) + if err != nil { + rpcsLog.Errorf("error while trying to acquire a buy "+ + "order for invoice: %v", err) + continue + } - default: - return nil, fmt.Errorf("unexpected response type: %T", r) + rate, err := rpcutils.UnmarshalFixedPoint( + &priceoraclerpc.FixedPoint{ + Coefficient: quote.AskAssetRate.Coefficient, + Scale: quote.AskAssetRate.Scale, + }, + ) + if err != nil { + return nil, err + } + + acquiredQuotes = append(acquiredQuotes, quoteWithInfo{ + quote: quote, + rate: rate, + // Since the channels are sorted, we know the value with + // the greatest remote balance is at index 0. + channel: channels[0], + }) } + // 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() + }) + + // If we failed to get any quotes, we need to return an error. If the + // user has already defined quotes in the request we don't return an + // error. + if len(acquiredQuotes) == 0 && !existingQuotes { + return nil, fmt.Errorf("could not create any quotes for the " + + "invoice") + } + + // We need to trim any extra quotes that cannot make it into the bolt11 + // invoice due to size limitations. + if len(acquiredQuotes) > maxRfqHopHints { + acquiredQuotes = acquiredQuotes[:maxRfqHopHints] + } + + // TODO(george): We want to cancel back quotes that didn't make it into + // the set. Need to add CancelOrder endpoints to RFQ manager. + + // 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. + var expensiveQuote *rfqrpc.PeerAcceptedBuyQuote + if !existingQuotes { + expensiveQuote = acquiredQuotes[0].quote + } + + // replace with above // Now that we have the accepted quote, we know the amount in (milli) // Satoshi that we need to pay. We can now update the invoice with this // amount. invoiceAmtMsat, err := validateInvoiceAmount( - acceptedQuote, req.AssetAmount, iReq, + expensiveQuote, req.AssetAmount, iReq, ) if err != nil { return nil, fmt.Errorf("error validating invoice amount: %w", @@ -8058,19 +8128,6 @@ func (r *rpcServer) AddInvoice(ctx context.Context, } iReq.ValueMsat = int64(invoiceAmtMsat) - // 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 { @@ -8080,24 +8137,21 @@ 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, []zpay32.HopHint{*hopHint}, + ) } payReq, err := r.cfg.Lnd.Invoices.AddHoldInvoice( @@ -8113,7 +8167,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 { @@ -8122,29 +8176,39 @@ 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 + } + + lnrpcHopHint := rfq.Zpay32HopHintToLnrpc(hopHint) + + routeHints = append(routeHints, &lnrpc.RouteHint{ HopHints: []*lnrpc.HopHint{ - hopHint, + lnrpcHopHint, }, - }, + }) + } + + // Only replace the route hints of the invoice request if the user has + // not already set them. + if !existingQuotes { + iReq.RouteHints = routeHints } rpcCtx, _, rawClient := r.cfg.Lnd.Client.RawClientWithMacAuth(ctx) @@ -8154,7 +8218,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context, } return &tchrpc.AddInvoiceResponse{ - AcceptedBuyQuote: acceptedQuote, + AcceptedBuyQuote: expensiveQuote, InvoiceResult: invoiceResp, }, nil } @@ -8300,6 +8364,57 @@ func validateInvoiceAmount(acceptedQuote *rfqrpc.PeerAcceptedBuyQuote, return newInvoiceAmtMsat, nil } +// acquireBuyOrder performs an RFQ negotiation with the target peer and quote +// parameters and returns the quote if the negotiation was successful. +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 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 @@ -8395,196 +8510,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, haveGroupChans, err := r.computeCompatibleChannelAssetBalance( - ctx, specifier, - ) - if err != nil { - return nil, fmt.Errorf("error computing available asset "+ - "channel balance: %w", err) - } - - // If the user uses the asset ID to specify what asset to use, that will - // not work for asset channels that have multiple UTXOs of grouped - // assets. The result wouldn't be deterministic anyway (meaning, it's - // not guaranteed that a specific asset ID is moved within a channel - // when an HTLC is sent, the allocation logic decides which actual UTXO - // is used). So we tell the user to use the group key instead, at least - // for channels that have multiple UTXOs of grouped assets. - if specifier.HasId() && len(balances) == 0 && haveGroupChans { - return nil, fmt.Errorf("no compatible asset channel found for "+ - "%s, make sure to use group key for grouped asset "+ - "channels", &specifier) - } - - // If the above special case didn't apply, it just means we don't have - // the specific asset in a channel that can be used. - 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.JsonAssetChannel -} - -// computeCompatibleChannelAssetBalance computes the total local and remote -// balance for each asset channel that matches the provided asset specifier. -// For a channel to match an asset specifier, all asset UTXOs in the channel -// must match the specifier. That means a channel is seen as _not_ compatible if -// it contains multiple asset IDs but the specifier only specifies one of them. -// The user should use the group key in that case instead. To help inform the -// user about this, the returned boolean value indicates if there are any active -// channels that have grouped assets. -func (r *rpcServer) computeCompatibleChannelAssetBalance(ctx context.Context, - specifier asset.Specifier) ([]channelWithSpecifier, bool, error) { - - activeChannels, err := r.cfg.Lnd.Client.ListChannels(ctx, true, false) - if err != nil { - return nil, false, fmt.Errorf("unable to fetch channels: %w", - err) - } - - var ( - channels = make([]channelWithSpecifier, 0) - haveGroupedAssetChannels bool - ) - 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, false, fmt.Errorf("unable to unmarshal "+ - "asset data: %w", err) - } - - if len(assetData.GroupKey) > 0 { - haveGroupedAssetChannels = true - } - - // Check if the assets of this channel match the provided - // specifier. - pass, err := r.cfg.RfqManager.ChannelMatchesFully( - ctx, assetData, specifier, - ) - if err != nil { - return nil, false, err - } - - if pass { - channels = append(channels, channelWithSpecifier{ - specifier: specifier, - channelInfo: openChan, - assetInfo: assetData, - }) - } - } - - return channels, haveGroupedAssetChannels, 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/tapcfg/server.go b/tapcfg/server.go index b2f5449a6..ae7e343e4 100644 --- a/tapcfg/server.go +++ b/tapcfg/server.go @@ -397,7 +397,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, HtlcInterceptor: lndRouterClient, HtlcSubscriber: lndRouterClient, PriceOracle: priceOracle, - ChannelLister: walletAnchor, + ChannelLister: lndServices.Client, GroupLookup: tapdbAddrBook, AliasManager: lndRouterClient, // nolint: lll diff --git a/tapchannel/aux_invoice_manager.go b/tapchannel/aux_invoice_manager.go index f18cfe556..a3045acb4 100644 --- a/tapchannel/aux_invoice_manager.go +++ b/tapchannel/aux_invoice_manager.go @@ -190,7 +190,7 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context, // accepting sats instead of assets. // // TODO(george): Strict-forwarding could be configurable? - if isAssetInvoice(req.Invoice, s) { + if IsAssetInvoice(req.Invoice, s) { iLog.Debugf("has no asset custom records, but " + "invoice requires assets, canceling HTLCs") @@ -202,7 +202,7 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context, return resp, nil // We have custom records, but the invoice is not an asset invoice. - case !isAssetInvoice(req.Invoice, s) && !req.Invoice.IsKeysend: + case !IsAssetInvoice(req.Invoice, s) && !req.Invoice.IsKeysend: // If we do have custom records, but the invoice does not // correspond to an asset invoice, we do not settle the invoice. // Since we requested btc we should be receiving btc. @@ -386,11 +386,11 @@ func (s *AuxInvoiceManager) RfqPeerFromScid(scid uint64) (route.Vertex, error) { return buyQuote.Peer, nil } -// isAssetInvoice checks whether the provided invoice is an asset invoice. This +// IsAssetInvoice checks whether the provided invoice is an asset invoice. This // method checks whether the routing hints of the invoice match those created // when generating an asset invoice, and if that's the case we then check that // the scid matches an existing quote. -func isAssetInvoice(invoice *lnrpc.Invoice, rfqLookup RfqLookup) bool { +func IsAssetInvoice(invoice *lnrpc.Invoice, rfqLookup RfqLookup) bool { hints := invoice.RouteHints for _, hint := range hints { diff --git a/tapchannel/aux_invoice_manager_test.go b/tapchannel/aux_invoice_manager_test.go index 415f31cc4..310101dac 100644 --- a/tapchannel/aux_invoice_manager_test.go +++ b/tapchannel/aux_invoice_manager_test.go @@ -226,7 +226,7 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context, } if !rfqmsg.HasAssetHTLCCustomRecords(r.WireCustomRecords) { - if isAssetInvoice(r.Invoice, m) { + if IsAssetInvoice(r.Invoice, m) { if !res.CancelSet { m.t.Errorf("expected cancel set flag") } @@ -236,7 +236,7 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context, if r.ExitHtlcAmt != res.AmtPaid { m.t.Errorf("AmtPaid != ExitHtlcAmt") } - } else if !isAssetInvoice(r.Invoice, m) { + } else if !IsAssetInvoice(r.Invoice, m) { if !res.CancelSet { m.t.Errorf("expected cancel set flag") } diff --git a/taprpc/tapchannelrpc/tapchannel.proto b/taprpc/tapchannelrpc/tapchannel.proto index d665afa47..77afd4893 100644 --- a/taprpc/tapchannelrpc/tapchannel.proto +++ b/taprpc/tapchannelrpc/tapchannel.proto @@ -37,7 +37,9 @@ service TaprootAssetChannels { /* litcli: `ln addinvoice` 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); diff --git a/taprpc/tapchannelrpc/tapchannel.swagger.json b/taprpc/tapchannelrpc/tapchannel.swagger.json index fb5f4ed59..bc5d5a050 100644 --- a/taprpc/tapchannelrpc/tapchannel.swagger.json +++ b/taprpc/tapchannelrpc/tapchannel.swagger.json @@ -84,7 +84,7 @@ }, "/v1/taproot-assets/channels/invoice": { "post": { - "summary": "litcli: `ln addinvoice`\nAddInvoice 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": "litcli: `ln addinvoice`\nAddInvoice 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": { diff --git a/taprpc/tapchannelrpc/tapchannel_grpc.pb.go b/taprpc/tapchannelrpc/tapchannel_grpc.pb.go index cffa39ab8..4d1302d89 100644 --- a/taprpc/tapchannelrpc/tapchannel_grpc.pb.go +++ b/taprpc/tapchannelrpc/tapchannel_grpc.pb.go @@ -37,7 +37,9 @@ type TaprootAssetChannelsClient interface { // litcli: `ln addinvoice` // 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) // litcli: `ln decodeassetinvoice` // DecodeAssetPayReq is similar to lnd's lnrpc.DecodePayReq, but it accepts an @@ -145,7 +147,9 @@ type TaprootAssetChannelsServer interface { // litcli: `ln addinvoice` // 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) // litcli: `ln decodeassetinvoice` // DecodeAssetPayReq is similar to lnd's lnrpc.DecodePayReq, but it accepts an diff --git a/wallet_anchor.go b/wallet_anchor.go index ed44d8a4b..78015102c 100644 --- a/wallet_anchor.go +++ b/wallet_anchor.go @@ -13,7 +13,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/lightninglabs/lndclient" - "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" "github.com/lightninglabs/taproot-assets/tapsend" @@ -237,4 +236,3 @@ func (l *LndRpcWalletAnchor) MinRelayFee( var _ tapgarden.WalletAnchor = (*LndRpcWalletAnchor)(nil) var _ tapfreighter.WalletAnchor = (*LndRpcWalletAnchor)(nil) -var _ rfq.ChannelLister = (*LndRpcWalletAnchor)(nil)