From 3ee5ec2e702fe7ae7021ab012546086c72370193 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 29 Jul 2024 18:17:57 +0200 Subject: [PATCH 1/3] mod+accounts: bump lndclient dep, split mocks Because each lndclient service mock now needs to implement the RawClientWithMacAuth method but with a different type, we can't implement two lndclient service mocks within a single struct, as that would require us to have two methods with the same name but different types. So we split the lnd and rounter mocks into two separate structs. --- accounts/checkers_test.go | 21 ++-- accounts/service_test.go | 213 ++++++++++++++++++++++++-------------- go.mod | 4 +- go.sum | 8 +- 4 files changed, 155 insertions(+), 91 deletions(-) diff --git a/accounts/checkers_test.go b/accounts/checkers_test.go index c2d503acf..19e3070e3 100644 --- a/accounts/checkers_test.go +++ b/accounts/checkers_test.go @@ -513,13 +513,14 @@ func testSendPayment(t *testing.T, uri string) { } lndMock := newMockLnd() + routerMock := newMockRouter() errFunc := func(err error) { lndMock.mainErrChan <- err } service, err := NewService(t.TempDir(), errFunc) require.NoError(t, err) - err = service.Start(lndMock, lndMock, chainParams) + err = service.Start(lndMock, routerMock, chainParams) require.NoError(t, err) assertBalance := func(id AccountID, expectedBalance int64) { @@ -615,7 +616,7 @@ func testSendPayment(t *testing.T, uri string) { require.NoError(t, err) assertBalance(acct.ID, 4000) - lndMock.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ + routerMock.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ testHash: {}, }) @@ -646,7 +647,7 @@ func testSendPayment(t *testing.T, uri string) { // was initiated. assertBalance(acct.ID, 4000) - lndMock.assertNoPaymentRequest(t) + routerMock.assertNoPaymentRequest(t) // The final test we will do is to have two send requests initiated // before the response for the first one has been received. @@ -708,13 +709,14 @@ func TestSendPaymentV2(t *testing.T) { } lndMock := newMockLnd() + routerMock := newMockRouter() errFunc := func(err error) { lndMock.mainErrChan <- err } service, err := NewService(t.TempDir(), errFunc) require.NoError(t, err) - err = service.Start(lndMock, lndMock, chainParams) + err = service.Start(lndMock, routerMock, chainParams) require.NoError(t, err) assertBalance := func(id AccountID, expectedBalance int64) { @@ -808,7 +810,7 @@ func TestSendPaymentV2(t *testing.T) { require.NoError(t, err) assertBalance(acct.ID, 4000) - lndMock.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ + routerMock.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ testHash: {}, }) @@ -836,7 +838,7 @@ func TestSendPaymentV2(t *testing.T) { // was initiated. assertBalance(acct.ID, 4000) - lndMock.assertNoPaymentRequest(t) + routerMock.assertNoPaymentRequest(t) // The final test we will do is to have two send requests initiated // before the response for the first one has been received. @@ -894,13 +896,14 @@ func TestSendToRouteV2(t *testing.T) { } lndMock := newMockLnd() + routerMock := newMockRouter() errFunc := func(err error) { lndMock.mainErrChan <- err } service, err := NewService(t.TempDir(), errFunc) require.NoError(t, err) - err = service.Start(lndMock, lndMock, chainParams) + err = service.Start(lndMock, routerMock, chainParams) require.NoError(t, err) assertBalance := func(id AccountID, expectedBalance int64) { @@ -998,7 +1001,7 @@ func TestSendToRouteV2(t *testing.T) { require.NoError(t, err) assertBalance(acct.ID, 4000) - lndMock.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ + routerMock.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ testHash: {}, }) @@ -1028,7 +1031,7 @@ func TestSendToRouteV2(t *testing.T) { // was initiated. assertBalance(acct.ID, 4000) - lndMock.assertNoPaymentRequest(t) + routerMock.assertNoPaymentRequest(t) // The final test we will do is to have two send requests initiated // before the response for the first one has been received. diff --git a/accounts/service_test.go b/accounts/service_test.go index 66fa5d266..2583f612e 100644 --- a/accounts/service_test.go +++ b/accounts/service_test.go @@ -10,6 +10,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" invpkg "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) @@ -31,14 +32,10 @@ type mockLnd struct { mainErrChan chan error invoiceReq chan lndclient.InvoiceSubscriptionRequest - paymentReq chan lntypes.Hash invoiceSubscriptionErr error - trackPaymentErr error invoiceErrChan chan error - paymentErrChan chan error invoiceChan chan *lndclient.Invoice - paymentChans map[lntypes.Hash]chan lndclient.PaymentStatus } func newMockLnd() *mockLnd { @@ -47,16 +44,19 @@ func newMockLnd() *mockLnd { invoiceReq: make( chan lndclient.InvoiceSubscriptionRequest, 10, ), - paymentReq: make(chan lntypes.Hash, 10), invoiceErrChan: make(chan error, 10), - paymentErrChan: make(chan error, 10), invoiceChan: make(chan *lndclient.Invoice), - paymentChans: make( - map[lntypes.Hash]chan lndclient.PaymentStatus, - ), } } +// RawClientWithMacAuth returns a context with the proper macaroon +// authentication, the default RPC timeout, and the raw client. +func (m *mockLnd) RawClientWithMacAuth(ctx context.Context) (context.Context, + time.Duration, lnrpc.LightningClient) { + + return ctx, 0, nil +} + func (m *mockLnd) assertNoMainErr(t *testing.T) { select { case err := <-m.mainErrChan: @@ -100,23 +100,69 @@ func (m *mockLnd) assertInvoiceRequest(t *testing.T, addIndex, } } -func (m *mockLnd) assertNoPaymentRequest(t *testing.T) { +// SubscribeInvoices allows a client to subscribe to updates of newly +// added/settled invoices. +func (m *mockLnd) SubscribeInvoices(_ context.Context, + req lndclient.InvoiceSubscriptionRequest) (<-chan *lndclient.Invoice, + <-chan error, error) { + + if m.invoiceSubscriptionErr != nil { + return nil, nil, m.invoiceSubscriptionErr + } + + m.invoiceReq <- req + + return m.invoiceChan, m.invoiceErrChan, nil +} + +type mockRouter struct { + lndclient.RouterClient + + mainErrChan chan error + + paymentReq chan lntypes.Hash + + trackPaymentErr error + paymentErrChan chan error + paymentChans map[lntypes.Hash]chan lndclient.PaymentStatus +} + +func newMockRouter() *mockRouter { + return &mockRouter{ + mainErrChan: make(chan error, 10), + paymentReq: make(chan lntypes.Hash, 10), + paymentErrChan: make(chan error, 10), + paymentChans: make( + map[lntypes.Hash]chan lndclient.PaymentStatus, + ), + } +} + +// RawClientWithMacAuth returns a context with the proper macaroon +// authentication, the default RPC timeout, and the raw client. +func (r *mockRouter) RawClientWithMacAuth(ctx context.Context) (context.Context, + time.Duration, routerrpc.RouterClient) { + + return ctx, 0, nil +} + +func (r *mockRouter) assertNoPaymentRequest(t *testing.T) { select { - case req := <-m.paymentReq: + case req := <-r.paymentReq: t.Fatalf("Expected no payment request, got %v", req) default: } } -func (m *mockLnd) assertPaymentRequests(t *testing.T, +func (r *mockRouter) assertPaymentRequests(t *testing.T, hashes map[lntypes.Hash]struct{}) { overallTimeout := time.After(testTimeout) for { select { - case hash := <-m.paymentReq: + case hash := <-r.paymentReq: require.Contains(t, hashes, hash) delete(hashes, hash) @@ -132,34 +178,19 @@ func (m *mockLnd) assertPaymentRequests(t *testing.T, } } -// SubscribeInvoices allows a client to subscribe to updates of newly -// added/settled invoices. -func (m *mockLnd) SubscribeInvoices(_ context.Context, - req lndclient.InvoiceSubscriptionRequest) (<-chan *lndclient.Invoice, - <-chan error, error) { - - if m.invoiceSubscriptionErr != nil { - return nil, nil, m.invoiceSubscriptionErr - } - - m.invoiceReq <- req - - return m.invoiceChan, m.invoiceErrChan, nil -} - // TrackPayment picks up a previously started payment and returns a payment // update stream and an error stream. -func (m *mockLnd) TrackPayment(_ context.Context, +func (r *mockRouter) TrackPayment(_ context.Context, hash lntypes.Hash) (chan lndclient.PaymentStatus, chan error, error) { - if m.trackPaymentErr != nil { - return nil, nil, m.trackPaymentErr + if r.trackPaymentErr != nil { + return nil, nil, r.trackPaymentErr } - m.paymentReq <- hash - m.paymentChans[hash] = make(chan lndclient.PaymentStatus, 1) + r.paymentReq <- hash + r.paymentChans[hash] = make(chan lndclient.PaymentStatus, 1) - return m.paymentChans[hash], m.paymentErrChan, nil + return r.paymentChans[hash], r.paymentErrChan, nil } // TestAccountService tests that the account service can track payments and @@ -169,18 +200,20 @@ func TestAccountService(t *testing.T) { testCases := []struct { name string - setup func(t *testing.T, lnd *mockLnd, + setup func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) startupErr string - validate func(t *testing.T, lnd *mockLnd, + validate func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) }{{ name: "startup err on invoice subscription", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + lnd.invoiceSubscriptionErr = testErr }, startupErr: testErr.Error(), - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { lnd.assertNoInvoiceRequest(t) @@ -188,7 +221,9 @@ func TestAccountService(t *testing.T) { }, }, { name: "err on invoice update", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + acct := &OffChainBalanceAccount{ ID: testID, Type: TypeInitialBalance, @@ -201,7 +236,7 @@ func TestAccountService(t *testing.T) { err := s.store.UpdateAccount(acct) require.NoError(t, err) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { // Start by closing the store. This should cause an @@ -232,7 +267,9 @@ func TestAccountService(t *testing.T) { }, }, { name: "err in invoice err channel", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + acct := &OffChainBalanceAccount{ ID: testID, Type: TypeInitialBalance, @@ -245,7 +282,7 @@ func TestAccountService(t *testing.T) { err := s.store.UpdateAccount(acct) require.NoError(t, err) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { // Ensure that the service was started successfully. require.True(t, s.IsRunning()) @@ -264,7 +301,9 @@ func TestAccountService(t *testing.T) { }, }, { name: "goroutine err sent on main err chan", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + acct := &OffChainBalanceAccount{ ID: testID, Type: TypeInitialBalance, @@ -280,7 +319,7 @@ func TestAccountService(t *testing.T) { s.mainErrCallback(testErr) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { lnd.assertInvoiceRequest(t, 0, 0) @@ -288,7 +327,9 @@ func TestAccountService(t *testing.T) { }, }, { name: "startup do not track completed payments", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + acct, err := s.store.NewAccount( 1234, testExpiration, "", ) @@ -303,18 +344,20 @@ func TestAccountService(t *testing.T) { err = s.store.UpdateAccount(acct) require.NoError(t, err) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { require.Contains(t, s.invoiceToAccount, testHash) - lnd.assertNoPaymentRequest(t) + r.assertNoPaymentRequest(t) lnd.assertInvoiceRequest(t, 0, 0) lnd.assertNoMainErr(t) require.True(t, s.IsRunning()) }, }, { name: "startup err on payment tracking", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + acct := &OffChainBalanceAccount{ ID: testID, Type: TypeInitialBalance, @@ -333,9 +376,9 @@ func TestAccountService(t *testing.T) { err := s.store.UpdateAccount(acct) require.NoError(t, err) - lnd.trackPaymentErr = testErr + r.trackPaymentErr = testErr }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { // Assert that the invoice subscription succeeded. @@ -348,11 +391,13 @@ func TestAccountService(t *testing.T) { // payment to pending payment, and that lnd isn't awaiting // the payment request. require.NotContains(t, s.pendingPayments, testHash) - lnd.assertNoPaymentRequest(t) + r.assertNoPaymentRequest(t) }, }, { name: "err on payment update", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + acct := &OffChainBalanceAccount{ ID: testID, Type: TypeInitialBalance, @@ -368,12 +413,13 @@ func TestAccountService(t *testing.T) { err := s.store.UpdateAccount(acct) require.NoError(t, err) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { + // Ensure that the service was started successfully, // and lnd contains the payment request. require.True(t, s.IsRunning()) - lnd.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ + r.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ testHash: {}, }) @@ -385,7 +431,7 @@ func TestAccountService(t *testing.T) { // Send an invalid payment over the payment chan // which should error and disable the service - lnd.paymentChans[testHash] <- lndclient.PaymentStatus{ + r.paymentChans[testHash] <- lndclient.PaymentStatus{ State: lnrpc.Payment_SUCCEEDED, Fee: 234, Value: 1000, @@ -402,7 +448,9 @@ func TestAccountService(t *testing.T) { }, }, { name: "err in payment update chan", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + acct := &OffChainBalanceAccount{ ID: testID, Type: TypeInitialBalance, @@ -418,18 +466,19 @@ func TestAccountService(t *testing.T) { err := s.store.UpdateAccount(acct) require.NoError(t, err) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { + // Ensure that the service was started successfully, // and lnd contains the payment request. require.True(t, s.IsRunning()) - lnd.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ + r.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ testHash: {}, }) // Now let's send an error over the payment error // channel. This should disable the service. - lnd.paymentErrChan <- testErr + r.paymentErrChan <- testErr // Ensure that the service was eventually disabled. assertEventually(t, func() bool { @@ -441,7 +490,9 @@ func TestAccountService(t *testing.T) { }, }, { name: "startup track in-flight payments", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + acct := &OffChainBalanceAccount{ ID: testID, Type: TypeInitialBalance, @@ -468,11 +519,11 @@ func TestAccountService(t *testing.T) { err := s.store.UpdateAccount(acct) require.NoError(t, err) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { require.Contains(t, s.invoiceToAccount, testHash) - lnd.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ + r.assertPaymentRequests(t, map[lntypes.Hash]struct{}{ testHash: {}, testHash2: {}, testHash3: {}, @@ -482,7 +533,7 @@ func TestAccountService(t *testing.T) { // Send an actual payment update and make sure the // amount is debited from the account. - lnd.paymentChans[testHash] <- lndclient.PaymentStatus{ + r.paymentChans[testHash] <- lndclient.PaymentStatus{ State: lnrpc.Payment_SUCCEEDED, Fee: 500, Value: 1500, @@ -498,7 +549,7 @@ func TestAccountService(t *testing.T) { // Remove the other payment and make sure it disappears // from the tracked payments and is also updated // correctly in the account store. - lnd.paymentChans[testHash2] <- lndclient.PaymentStatus{ + r.paymentChans[testHash2] <- lndclient.PaymentStatus{ State: lnrpc.Payment_FAILED, Fee: 0, Value: 1000, @@ -538,7 +589,7 @@ func TestAccountService(t *testing.T) { require.ErrorIs(t, err, ErrAccBalanceInsufficient) // Now signal that the payment was non-initiated. - lnd.paymentErrChan <- channeldb.ErrPaymentNotInitiated + r.paymentErrChan <- channeldb.ErrPaymentNotInitiated // Once the error is handled in the service.TrackPayment // goroutine, and therefore free up the 2000 in-flight @@ -572,11 +623,13 @@ func TestAccountService(t *testing.T) { }, }, { name: "keep track of invoice indexes", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + err := s.store.StoreLastIndexes(987_654, 555_555) require.NoError(t, err) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { // We expect the initial subscription to start at the @@ -621,7 +674,9 @@ func TestAccountService(t *testing.T) { }, }, { name: "credit account", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + acct := &OffChainBalanceAccount{ ID: testID, Type: TypeInitialBalance, @@ -636,7 +691,7 @@ func TestAccountService(t *testing.T) { err := s.store.UpdateAccount(acct) require.NoError(t, err) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { lnd.assertInvoiceRequest(t, 0, 0) @@ -676,7 +731,9 @@ func TestAccountService(t *testing.T) { }, }, { name: "in-flight payments", - setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { + setup: func(t *testing.T, lnd *mockLnd, r *mockRouter, + s *InterceptorService) { + // We set up two accounts with a balance of 5k msats. // The first account has two in-flight payments, one of @@ -723,7 +780,7 @@ func TestAccountService(t *testing.T) { err = s.store.UpdateAccount(acct2) require.NoError(t, err) }, - validate: func(t *testing.T, lnd *mockLnd, + validate: func(t *testing.T, lnd *mockLnd, r *mockRouter, s *InterceptorService) { // The first should be able to initiate another payment @@ -739,7 +796,7 @@ func TestAccountService(t *testing.T) { // Remove one of the payments (to simulate it failed) // and try again. - lnd.paymentChans[testHash] <- lndclient.PaymentStatus{ + r.paymentChans[testHash] <- lndclient.PaymentStatus{ State: lnrpc.Payment_FAILED, } @@ -767,6 +824,7 @@ func TestAccountService(t *testing.T) { tt.Parallel() lndMock := newMockLnd() + routerMock := newMockRouter() errFunc := func(err error) { lndMock.mainErrChan <- err } @@ -776,18 +834,21 @@ func TestAccountService(t *testing.T) { // Is a setup call required to initialize initial // conditions? if tc.setup != nil { - tc.setup(t, lndMock, service) + tc.setup(t, lndMock, routerMock, service) } // Any errors during startup expected? - err = service.Start(lndMock, lndMock, chainParams) + err = service.Start(lndMock, routerMock, chainParams) if tc.startupErr != "" { require.ErrorContains(tt, err, tc.startupErr) lndMock.assertNoMainErr(t) if tc.validate != nil { - tc.validate(tt, lndMock, service) + tc.validate( + tt, lndMock, routerMock, + service, + ) } return @@ -795,7 +856,7 @@ func TestAccountService(t *testing.T) { // Any post execution validation that we need to run? if tc.validate != nil { - tc.validate(tt, lndMock, service) + tc.validate(tt, lndMock, routerMock, service) } err = service.Stop() diff --git a/go.mod b/go.mod index 784805731..c7bb04516 100644 --- a/go.mod +++ b/go.mod @@ -15,8 +15,8 @@ require ( github.com/lightninglabs/faraday v0.2.13-alpha github.com/lightninglabs/lightning-node-connect v0.3.1-alpha github.com/lightninglabs/lightning-terminal/autopilotserverrpc v0.0.1 - github.com/lightninglabs/lndclient v1.0.1-0.20240607082608-4ce52a1a3f27 - github.com/lightninglabs/loop v0.28.5-beta.0.20240607082710-7368d048da05 + github.com/lightninglabs/lndclient v1.0.1-0.20240724144614-a676c76e9eaa + github.com/lightninglabs/loop v0.28.6-beta.0.20240729115851-63e976ab27a4 github.com/lightninglabs/loop/swapserverrpc v1.0.8 github.com/lightninglabs/pool v0.6.5-beta.0.20240604070222-e121aadb3289 github.com/lightninglabs/pool/auctioneerrpc v1.1.2 diff --git a/go.sum b/go.sum index 548200a09..55d86d2be 100644 --- a/go.sum +++ b/go.sum @@ -1159,10 +1159,10 @@ github.com/lightninglabs/lightning-node-connect v0.3.1-alpha h1:fean3EXsohrpRmrc github.com/lightninglabs/lightning-node-connect v0.3.1-alpha/go.mod h1:TC+tFEPlJxU4+TU5UW/TKAfyav/+AZHHaV0nD02LVjk= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 h1:Er1miPZD2XZwcfE4xoS5AILqP1mj7kqnhbBSxW9BDxY= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4= -github.com/lightninglabs/lndclient v1.0.1-0.20240607082608-4ce52a1a3f27 h1:vm8a13EzH2Qe6j4eZx+tHPeEVoNhJ7coihFPX6K2kco= -github.com/lightninglabs/lndclient v1.0.1-0.20240607082608-4ce52a1a3f27/go.mod h1:bxd2a15cIaW8KKcmOf9nNDI/GTxxj0upEYs1EIkttqw= -github.com/lightninglabs/loop v0.28.5-beta.0.20240607082710-7368d048da05 h1:EU0k3EwoF+iXLA3VBor4qZNKJH7FKdQMCUqKVpDdtEc= -github.com/lightninglabs/loop v0.28.5-beta.0.20240607082710-7368d048da05/go.mod h1:YhiTlh/yqWNgyAeYvISWV+3h2daKPXr7pfNPf5wJ7H0= +github.com/lightninglabs/lndclient v1.0.1-0.20240724144614-a676c76e9eaa h1:Yf3V7e6jVfGEjamRY4mTiAq+flbnasx97+66zHVoXX0= +github.com/lightninglabs/lndclient v1.0.1-0.20240724144614-a676c76e9eaa/go.mod h1:bxd2a15cIaW8KKcmOf9nNDI/GTxxj0upEYs1EIkttqw= +github.com/lightninglabs/loop v0.28.6-beta.0.20240729115851-63e976ab27a4 h1:Sr7rXR6zK5EaMVNP+3fMK32X1nqMrHJF8nFBsT6UO9o= +github.com/lightninglabs/loop v0.28.6-beta.0.20240729115851-63e976ab27a4/go.mod h1:DxPSqqESqDRuJB4FJpCeVpOA7qSx7PXi04FFmiIbDqQ= github.com/lightninglabs/loop/swapserverrpc v1.0.8 h1:bk7dDGuA3JQUsMDqZNyAy5Pcw5xS9jforz7YnyeSxKM= github.com/lightninglabs/loop/swapserverrpc v1.0.8/go.mod h1:Ml3gMwe/iTRLvu1QGGZzXcr0DYSa9sJGwKPktLaWtwE= github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd h1:D8aRocHpoCv43hL8egXEMYyPmyOiefFHZ66338KQB2s= From a641b68d0a8c1dba613e440cb8f822a064a2901f Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 29 Jul 2024 15:21:37 +0200 Subject: [PATCH 2/3] multi: bump deps, use new tapchannelrpc.SendPayment RPC With this commit we bump to the latest version of taproot assets and lnd (staging branch), so we can use the new SendPayment RPC that handles the RFQ part inline. --- cmd/litcli/ln.go | 282 ++++++++++++++--------------- go.mod | 6 +- go.sum | 12 +- itest/assets_test.go | 157 ++++++---------- itest/litd_accounts_test.go | 28 +++ itest/litd_custom_channels_test.go | 4 +- 6 files changed, 231 insertions(+), 258 deletions(-) diff --git a/cmd/litcli/ln.go b/cmd/litcli/ln.go index c1aca7a51..473d43354 100644 --- a/cmd/litcli/ln.go +++ b/cmd/litcli/ln.go @@ -23,6 +23,7 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/urfave/cli" + "google.golang.org/grpc" ) const ( @@ -223,8 +224,66 @@ var ( Usage: "the asset ID of the asset to use when sending " + "payments with assets", } + + assetAmountFlag = cli.Uint64Flag{ + Name: "asset_amount", + Usage: "the amount of the asset to send in the asset keysend " + + "payment", + } + + rfqPeerPubKeyFlag = cli.StringFlag{ + Name: "rfq_peer_pubkey", + Usage: "(optional) the public key of the peer to ask for a " + + "quote when converting from assets to sats; must be " + + "set if there are multiple channels with the same " + + "asset ID present", + } ) +// resultStreamWrapper is a wrapper around the SendPaymentClient stream that +// implements the generic PaymentResultStream interface. +type resultStreamWrapper struct { + amountMsat int64 + stream tchrpc.TaprootAssetChannels_SendPaymentClient +} + +// Recv receives the next payment result from the stream. +// +// NOTE: This method is part of the PaymentResultStream interface. +func (w *resultStreamWrapper) Recv() (*lnrpc.Payment, error) { + resp, err := w.stream.Recv() + if err != nil { + return nil, err + } + + res := resp.Result + switch r := res.(type) { + // The very first response might be an accepted sell order, which we + // just print out. + case *tchrpc.SendPaymentResponse_AcceptedSellOrder: + quote := r.AcceptedSellOrder + msatPerUnit := quote.BidPrice + numUnits := uint64(w.amountMsat) / msatPerUnit + + fmt.Printf("Got quote for %v asset units at %v msat/unit from "+ + "peer %s with SCID %d\n", numUnits, msatPerUnit, + quote.Peer, quote.Scid) + + resp, err = w.stream.Recv() + if err != nil { + return nil, err + } + + return resp.GetPaymentResult(), nil + + case *tchrpc.SendPaymentResponse_PaymentResult: + return r.PaymentResult, nil + + default: + return nil, fmt.Errorf("unexpected response type: %T", r) + } +} + var sendPaymentCommand = cli.Command{ Name: "sendpayment", Category: commands.SendPaymentCommand.Category, @@ -236,14 +295,16 @@ var sendPaymentCommand = cli.Command{ Note that this will only work in concert with the --keysend argument. `, - ArgsUsage: commands.SendPaymentCommand.ArgsUsage + " --asset_id=X", - Flags: append(commands.SendPaymentCommand.Flags, assetIDFlag), - Action: sendPayment, + ArgsUsage: commands.SendPaymentCommand.ArgsUsage + " --asset_id=X " + + "--asset_amount=Y [--rfq_peer_pubkey=Z]", + Flags: append( + commands.SendPaymentCommand.Flags, assetIDFlag, assetAmountFlag, + rfqPeerPubKeyFlag, + ), + Action: sendPayment, } func sendPayment(ctx *cli.Context) error { - ctxb := context.Background() - // Show command help if no arguments provided if ctx.NArg() == 0 && ctx.NumFlags() == 0 { _ = cli.ShowCommandHelp(ctx, "sendpayment") @@ -254,67 +315,32 @@ func sendPayment(ctx *cli.Context) error { if err != nil { return fmt.Errorf("unable to make rpc con: %w", err) } - defer cleanup() - lndClient := lnrpc.NewLightningClient(lndConn) + tapdConn, cleanup, err := connectTapdClient(ctx) + if err != nil { + return fmt.Errorf("error creating tapd connection: %w", err) + } + defer cleanup() switch { case !ctx.IsSet(assetIDFlag.Name): return fmt.Errorf("the --asset_id flag must be set") case !ctx.IsSet("keysend"): return fmt.Errorf("the --keysend flag must be set") - case !ctx.IsSet("amt"): - return fmt.Errorf("--amt must be set") + case !ctx.IsSet(assetAmountFlag.Name): + return fmt.Errorf("--asset_amount must be set") } assetIDStr := ctx.String(assetIDFlag.Name) - _, err = hex.DecodeString(assetIDStr) + assetIDBytes, err := hex.DecodeString(assetIDStr) if err != nil { return fmt.Errorf("unable to decode assetID: %v", err) } - // First, based on the asset ID and amount, we'll make sure that this - // channel even has enough funds to send. - assetBalances, err := computeAssetBalances(lndClient) - if err != nil { - return fmt.Errorf("unable to compute asset balances: %w", err) - } - - balance, ok := assetBalances.Assets[assetIDStr] - if !ok { - return fmt.Errorf("unable to send asset_id=%v, not in "+ - "channel", assetIDStr) - } - - amtToSend := ctx.Uint64("amt") - if amtToSend > balance.LocalBalance { - return fmt.Errorf("insufficient balance, want to send %v, "+ - "only have %v", amtToSend, balance.LocalBalance) - } - - tapdConn, cleanup, err := connectTapdClient(ctx) - if err != nil { - return fmt.Errorf("error creating tapd connection: %w", err) - } - defer cleanup() - - tchrpcClient := tchrpc.NewTaprootAssetChannelsClient(tapdConn) - - encodeReq := &tchrpc.EncodeCustomRecordsRequest_RouterSendPayment{ - RouterSendPayment: &tchrpc.RouterSendPaymentData{ - AssetAmounts: map[string]uint64{ - assetIDStr: amtToSend, - }, - }, - } - encodeResp, err := tchrpcClient.EncodeCustomRecords( - ctxb, &tchrpc.EncodeCustomRecordsRequest{ - Input: encodeReq, - }, - ) - if err != nil { - return fmt.Errorf("error encoding custom records: %w", err) + assetAmountToSend := ctx.Uint64(assetAmountFlag.Name) + if assetAmountToSend == 0 { + return fmt.Errorf("must specify asset amount to send") } // With the asset specific work out of the way, we'll parse the rest of @@ -339,15 +365,20 @@ func sendPayment(ctx *cli.Context) error { "is instead: %v", len(destNode)) } + rfqPeerKey, err := hex.DecodeString(ctx.String(rfqPeerPubKeyFlag.Name)) + if err != nil { + return fmt.Errorf("unable to decode RFQ peer public key: "+ + "%w", err) + } + // We use a constant amount of 500 to carry the asset HTLCs. In the // future, we can use the double HTLC trick here, though it consumes // more commitment space. const htlcCarrierAmt = 500 req := &routerrpc.SendPaymentRequest{ - Dest: destNode, - Amt: htlcCarrierAmt, - DestCustomRecords: make(map[uint64][]byte), - FirstHopCustomRecords: encodeResp.CustomRecords, + Dest: destNode, + Amt: htlcCarrierAmt, + DestCustomRecords: make(map[uint64][]byte), } if ctx.IsSet("payment_hash") { @@ -370,7 +401,33 @@ func sendPayment(ctx *cli.Context) error { req.PaymentHash = rHash - return commands.SendPaymentRequest(ctx, req) + return commands.SendPaymentRequest( + ctx, req, lndConn, tapdConn, func(ctx context.Context, + payConn grpc.ClientConnInterface, + req *routerrpc.SendPaymentRequest) ( + commands.PaymentResultStream, error) { + + tchrpcClient := tchrpc.NewTaprootAssetChannelsClient( + payConn, + ) + + stream, err := tchrpcClient.SendPayment( + ctx, &tchrpc.SendPaymentRequest{ + AssetId: assetIDBytes, + AssetAmount: assetAmountToSend, + PeerPubkey: rfqPeerKey, + PaymentRequest: req, + }, + ) + if err != nil { + return nil, err + } + + return &resultStreamWrapper{ + stream: stream, + }, nil + }, + ) } var payInvoiceCommand = cli.Command{ @@ -434,24 +491,6 @@ func payInvoice(ctx *cli.Context) error { return fmt.Errorf("unable to decode assetID: %v", err) } - // First, based on the asset ID and amount, we'll make sure that this - // channel even has enough funds to send. - assetBalances, err := computeAssetBalances(lndClient) - if err != nil { - return fmt.Errorf("unable to compute asset balances: %w", err) - } - - balance, ok := assetBalances.Assets[assetIDStr] - if !ok { - return fmt.Errorf("unable to send asset_id=%v, not in "+ - "channel", assetIDStr) - } - - if balance.LocalBalance == 0 { - return fmt.Errorf("no asset balance available for asset_id=%v", - assetIDStr) - } - var assetID asset.ID copy(assetID[:], assetIDBytes) @@ -462,88 +501,35 @@ func payInvoice(ctx *cli.Context) error { defer cleanup() - peerPubKey, err := hex.DecodeString(balance.Channel.RemotePubkey) - if err != nil { - return fmt.Errorf("unable to decode peer pubkey: %w", err) + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: commands.StripPrefix(payReq), } - rfqClient := rfqrpc.NewRfqClient(tapdConn) + return commands.SendPaymentRequest( + ctx, req, lndConn, tapdConn, func(ctx context.Context, + payConn grpc.ClientConnInterface, + req *routerrpc.SendPaymentRequest) ( + commands.PaymentResultStream, error) { - timeoutSeconds := uint32(60) - fmt.Printf("Asking peer %x for quote to sell assets to pay for "+ - "invoice over %d msats; waiting up to %ds\n", peerPubKey, - decodeResp.NumMsat, timeoutSeconds) + tchrpcClient := tchrpc.NewTaprootAssetChannelsClient( + payConn, + ) - resp, err := rfqClient.AddAssetSellOrder( - ctxb, &rfqrpc.AddAssetSellOrderRequest{ - AssetSpecifier: &rfqrpc.AssetSpecifier{ - Id: &rfqrpc.AssetSpecifier_AssetIdStr{ - AssetIdStr: assetIDStr, + stream, err := tchrpcClient.SendPayment( + ctx, &tchrpc.SendPaymentRequest{ + AssetId: assetIDBytes, }, - }, - // TODO(guggero): This should actually be the max BTC - // amount (invoice amount plus fee limit) in - // milli-satoshi, not the asset amount. Need to change - // the whole RFQ API to do that though. - MaxAssetAmount: balance.LocalBalance, - MinAsk: uint64(decodeResp.NumMsat), - Expiry: uint64(decodeResp.Expiry), - PeerPubKey: peerPubKey, - TimeoutSeconds: timeoutSeconds, - }, - ) - if err != nil { - return fmt.Errorf("error adding sell order: %w", err) - } - - var acceptedQuote *rfqrpc.PeerAcceptedSellQuote - switch r := resp.Response.(type) { - case *rfqrpc.AddAssetSellOrderResponse_AcceptedQuote: - acceptedQuote = r.AcceptedQuote - - case *rfqrpc.AddAssetSellOrderResponse_InvalidQuote: - return fmt.Errorf("peer %v sent back an invalid quote, "+ - "status: %v", r.InvalidQuote.Peer, - r.InvalidQuote.Status.String()) - - case *rfqrpc.AddAssetSellOrderResponse_RejectedQuote: - return fmt.Errorf("peer %v rejected the quote, code: %v, "+ - "error message: %v", r.RejectedQuote.Peer, - r.RejectedQuote.ErrorCode, r.RejectedQuote.ErrorMessage) - - default: - return fmt.Errorf("unexpected response type: %T", r) - } - - msatPerUnit := acceptedQuote.BidPrice - numUnits := uint64(decodeResp.NumMsat) / msatPerUnit - - fmt.Printf("Got quote for %v asset units at %v msat/unit from peer "+ - "%x with SCID %d\n", numUnits, msatPerUnit, peerPubKey, - acceptedQuote.Scid) - - tchrpcClient := tchrpc.NewTaprootAssetChannelsClient(tapdConn) + ) + if err != nil { + return nil, err + } - encodeReq := &tchrpc.EncodeCustomRecordsRequest_RouterSendPayment{ - RouterSendPayment: &tchrpc.RouterSendPaymentData{ - RfqId: acceptedQuote.Id, - }, - } - encodeResp, err := tchrpcClient.EncodeCustomRecords( - ctxb, &tchrpc.EncodeCustomRecordsRequest{ - Input: encodeReq, + return &resultStreamWrapper{ + amountMsat: decodeResp.NumMsat, + stream: stream, + }, nil }, ) - if err != nil { - return fmt.Errorf("error encoding custom records: %w", err) - } - - req := &routerrpc.SendPaymentRequest{ - PaymentRequest: commands.StripPrefix(payReq), - FirstHopCustomRecords: encodeResp.CustomRecords, - } - - return commands.SendPaymentRequest(ctx, req) } var addInvoiceCommand = cli.Command{ diff --git a/go.mod b/go.mod index c7bb04516..8f624d700 100644 --- a/go.mod +++ b/go.mod @@ -15,13 +15,13 @@ require ( github.com/lightninglabs/faraday v0.2.13-alpha github.com/lightninglabs/lightning-node-connect v0.3.1-alpha github.com/lightninglabs/lightning-terminal/autopilotserverrpc v0.0.1 - github.com/lightninglabs/lndclient v1.0.1-0.20240724144614-a676c76e9eaa + github.com/lightninglabs/lndclient v1.0.1-0.20240725080034-64a756aa4c36 github.com/lightninglabs/loop v0.28.6-beta.0.20240729115851-63e976ab27a4 github.com/lightninglabs/loop/swapserverrpc v1.0.8 github.com/lightninglabs/pool v0.6.5-beta.0.20240604070222-e121aadb3289 github.com/lightninglabs/pool/auctioneerrpc v1.1.2 - github.com/lightninglabs/taproot-assets v0.4.1 - github.com/lightningnetwork/lnd v0.18.0-beta.rc4.0.20240723043204-f09d4042aee4 + github.com/lightninglabs/taproot-assets v0.4.2-0.20240807122703-23c09ff3b017 + github.com/lightningnetwork/lnd v0.18.0-beta.rc4.0.20240730143253-1b353b0bfd58 github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/fn v1.1.0 github.com/lightningnetwork/lnd/kvdb v1.4.8 diff --git a/go.sum b/go.sum index 55d86d2be..018167980 100644 --- a/go.sum +++ b/go.sum @@ -1159,8 +1159,8 @@ github.com/lightninglabs/lightning-node-connect v0.3.1-alpha h1:fean3EXsohrpRmrc github.com/lightninglabs/lightning-node-connect v0.3.1-alpha/go.mod h1:TC+tFEPlJxU4+TU5UW/TKAfyav/+AZHHaV0nD02LVjk= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 h1:Er1miPZD2XZwcfE4xoS5AILqP1mj7kqnhbBSxW9BDxY= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4= -github.com/lightninglabs/lndclient v1.0.1-0.20240724144614-a676c76e9eaa h1:Yf3V7e6jVfGEjamRY4mTiAq+flbnasx97+66zHVoXX0= -github.com/lightninglabs/lndclient v1.0.1-0.20240724144614-a676c76e9eaa/go.mod h1:bxd2a15cIaW8KKcmOf9nNDI/GTxxj0upEYs1EIkttqw= +github.com/lightninglabs/lndclient v1.0.1-0.20240725080034-64a756aa4c36 h1:gfJ3TOuqSnuXEo1Boj1H9P6tpxPSH9cvi+rB10L0svI= +github.com/lightninglabs/lndclient v1.0.1-0.20240725080034-64a756aa4c36/go.mod h1:bxd2a15cIaW8KKcmOf9nNDI/GTxxj0upEYs1EIkttqw= github.com/lightninglabs/loop v0.28.6-beta.0.20240729115851-63e976ab27a4 h1:Sr7rXR6zK5EaMVNP+3fMK32X1nqMrHJF8nFBsT6UO9o= github.com/lightninglabs/loop v0.28.6-beta.0.20240729115851-63e976ab27a4/go.mod h1:DxPSqqESqDRuJB4FJpCeVpOA7qSx7PXi04FFmiIbDqQ= github.com/lightninglabs/loop/swapserverrpc v1.0.8 h1:bk7dDGuA3JQUsMDqZNyAy5Pcw5xS9jforz7YnyeSxKM= @@ -1175,12 +1175,12 @@ github.com/lightninglabs/pool/auctioneerrpc v1.1.2 h1:Dbg+9Z9jXnhimR27EN37foc4aB github.com/lightninglabs/pool/auctioneerrpc v1.1.2/go.mod h1:1wKDzN2zEP8srOi0B9iySlEsPdoPhw6oo3Vbm1v4Mhw= github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display h1:pRdza2wleRN1L2fJXd6ZoQ9ZegVFTAb2bOQfruJPKcY= github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -github.com/lightninglabs/taproot-assets v0.4.1 h1:XtOIbw2sUl0GkjWKgZXxopU76BCjxXMW1Dqj8Ia9dGI= -github.com/lightninglabs/taproot-assets v0.4.1/go.mod h1:9ne4bQJkvp9b6Ez0DZhmDzXM/vbBKAHw6v3zc2FqTFQ= +github.com/lightninglabs/taproot-assets v0.4.2-0.20240807122703-23c09ff3b017 h1:CKR/T9gnNreVbdIkuxHymC+JTnPynnluD2Z/D761RlE= +github.com/lightninglabs/taproot-assets v0.4.2-0.20240807122703-23c09ff3b017/go.mod h1:PMlRq9aKXaxs6PMeLnj3y3YnofrylNvEOTxvegTbhSc= github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f h1:Pua7+5TcFEJXIIZ1I2YAUapmbcttmLj4TTi786bIi3s= github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.18.0-beta.rc4.0.20240723043204-f09d4042aee4 h1:LPnz0JxnzXJvCro714eBanzO7FKx5HF0ldU++zIu9yY= -github.com/lightningnetwork/lnd v0.18.0-beta.rc4.0.20240723043204-f09d4042aee4/go.mod h1:0gen58n0DVnqJJqCMN3AXNtqWRT0KltQanlvehnhCq0= +github.com/lightningnetwork/lnd v0.18.0-beta.rc4.0.20240730143253-1b353b0bfd58 h1:qmJAHLGfpeYIl1qUKyQViOjNAVRqF4afKuORzeIAwjA= +github.com/lightningnetwork/lnd v0.18.0-beta.rc4.0.20240730143253-1b353b0bfd58/go.mod h1:0gen58n0DVnqJJqCMN3AXNtqWRT0KltQanlvehnhCq0= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= diff --git a/itest/assets_test.go b/itest/assets_test.go index c8d8c3fbb..845342ea7 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -584,31 +584,40 @@ func sendAssetKeySendPayment(t *testing.T, src, dst *HarnessNode, amt uint64, srcTapd := newTapClient(t, src) - // Now that we know the amount we need to send, we'll convert that into - // an HTLC tlv, which'll be used as the first hop TLV value. - encodeReq := &tchrpc.EncodeCustomRecordsRequest_RouterSendPayment{ - RouterSendPayment: &tchrpc.RouterSendPaymentData{ - AssetAmounts: map[string]uint64{ - hex.EncodeToString(assetID): amt, - }, - }, + // Read out the custom preimage for the keysend payment. + var preimage lntypes.Preimage + _, err := rand.Read(preimage[:]) + require.NoError(t, err) + + hash := preimage.Hash() + + // Set the preimage. If the user supplied a preimage with the data + // flag, the preimage that is set here will be overwritten later. + customRecords := make(map[uint64][]byte) + customRecords[record.KeySendType] = preimage[:] + + sendReq := &routerrpc.SendPaymentRequest{ + Dest: dst.PubKey[:], + Amt: btcAmt.UnwrapOr(500), + DestCustomRecords: customRecords, + PaymentHash: hash[:], + TimeoutSeconds: 3, } - encodeResp, err := srcTapd.EncodeCustomRecords( - ctxt, &tchrpc.EncodeCustomRecordsRequest{ - Input: encodeReq, - }, - ) + + stream, err := srcTapd.SendPayment(ctxt, &tchrpc.SendPaymentRequest{ + AssetId: assetID, + AssetAmount: amt, + PaymentRequest: sendReq, + }) require.NoError(t, err) - htlcCarrierAmt := btcAmt.UnwrapOr(500) - sendKeySendPayment( - t, src, dst, btcutil.Amount(htlcCarrierAmt), - encodeResp.CustomRecords, - ) + result, err := getAssetPaymentResult(stream) + require.NoError(t, err) + require.Equal(t, lnrpc.Payment_SUCCEEDED, result.Status) } -func sendKeySendPayment(t *testing.T, src, dst *HarnessNode, amt btcutil.Amount, - firstHopCustomRecords map[uint64][]byte) { +func sendKeySendPayment(t *testing.T, src, dst *HarnessNode, + amt btcutil.Amount) { ctxb := context.Background() ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) @@ -627,19 +636,16 @@ func sendKeySendPayment(t *testing.T, src, dst *HarnessNode, amt btcutil.Amount, customRecords[record.KeySendType] = preimage[:] req := &routerrpc.SendPaymentRequest{ - Dest: dst.PubKey[:], - Amt: int64(amt), - DestCustomRecords: customRecords, - FirstHopCustomRecords: firstHopCustomRecords, - PaymentHash: hash[:], - TimeoutSeconds: 3, + Dest: dst.PubKey[:], + Amt: int64(amt), + DestCustomRecords: customRecords, + PaymentHash: hash[:], + TimeoutSeconds: 3, } stream, err := src.RouterClient.SendPaymentV2(ctxt, req) require.NoError(t, err) - time.Sleep(time.Second) - result, err := getPaymentResult(stream) require.NoError(t, err) require.Equal(t, lnrpc.Payment_SUCCEEDED, result.Status) @@ -699,8 +705,6 @@ func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode, stream, err := payer.RouterClient.SendPaymentV2(ctxt, sendReq) require.NoError(t, err) - time.Sleep(time.Second) - result, err := getPaymentResult(stream) require.NoError(t, err) require.Equal(t, lnrpc.Payment_SUCCEEDED, result.Status) @@ -721,85 +725,40 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, }) require.NoError(t, err) - balancePayer, err := getChannelCustomData(payer, rfqPeer) - require.NoError(t, err) - - timeoutSeconds := uint32(60) - resp, err := payerTapd.AddAssetSellOrder( - ctxb, &rfqrpc.AddAssetSellOrderRequest{ - AssetSpecifier: &rfqrpc.AssetSpecifier{ - Id: &rfqrpc.AssetSpecifier_AssetId{ - AssetId: assetID, - }, - }, - // TODO(guggero): This should actually be the max BTC - // amount (invoice amount plus fee limit) in - // milli-satoshi, not the asset amount. Need to change - // the whole RFQ API to do that though. - MaxAssetAmount: balancePayer.LocalBalance, - MinAsk: uint64(decodedInvoice.NumMsat), - Expiry: uint64(decodedInvoice.Expiry), - PeerPubKey: rfqPeer.PubKey[:], - TimeoutSeconds: timeoutSeconds, - }, - ) - require.NoError(t, err) - - var acceptedQuote *rfqrpc.PeerAcceptedSellQuote - switch r := resp.Response.(type) { - case *rfqrpc.AddAssetSellOrderResponse_AcceptedQuote: - acceptedQuote = r.AcceptedQuote - - case *rfqrpc.AddAssetSellOrderResponse_InvalidQuote: - t.Fatalf("peer %v sent back an invalid quote, "+ - "status: %v", r.InvalidQuote.Peer, - r.InvalidQuote.Status.String()) - - case *rfqrpc.AddAssetSellOrderResponse_RejectedQuote: - t.Fatalf("peer %v rejected the quote, code: %v, "+ - "error message: %v", r.RejectedQuote.Peer, - r.RejectedQuote.ErrorCode, r.RejectedQuote.ErrorMessage) - - default: - t.Fatalf("unexpected response type: %T", r) - } - - mSatPerUnit := acceptedQuote.BidPrice - numUnits := uint64(decodedInvoice.NumMsat) / mSatPerUnit - - t.Logf("Got quote for %v asset units at %v msat/unit from peer "+ - "%x with SCID %d", numUnits, mSatPerUnit, rfqPeer.PubKey[:], - acceptedQuote.Scid) - - encodeReq := &tchrpc.EncodeCustomRecordsRequest_RouterSendPayment{ - RouterSendPayment: &tchrpc.RouterSendPaymentData{ - RfqId: acceptedQuote.Id, - }, - } - encodeResp, err := payerTapd.EncodeCustomRecords( - ctxt, &tchrpc.EncodeCustomRecordsRequest{ - Input: encodeReq, - }, - ) - require.NoError(t, err) - sendReq := &routerrpc.SendPaymentRequest{ - PaymentRequest: invoice.PaymentRequest, - TimeoutSeconds: 2, - FirstHopCustomRecords: encodeResp.CustomRecords, - FeeLimitMsat: 1_000_000, + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: 2, + FeeLimitMsat: 1_000_000, } if smallShards { sendReq.MaxShardSizeMsat = 80_000_000 } - stream, err := payer.RouterClient.SendPaymentV2(ctxt, sendReq) + stream, err := payerTapd.SendPayment(ctxt, &tchrpc.SendPaymentRequest{ + AssetId: assetID, + PeerPubkey: rfqPeer.PubKey[:], + PaymentRequest: sendReq, + }) + require.NoError(t, err) + + // We want to receive the accepted quote message first, so we know how + // many assets we're going to pay. + quoteMsg, err := stream.Recv() require.NoError(t, err) + acceptedQuote := quoteMsg.GetAcceptedSellOrder() + require.NotNil(t, acceptedQuote) - time.Sleep(time.Second) + peerPubKey := acceptedQuote.Peer + require.Equal(t, peerPubKey, rfqPeer.PubKeyStr) - result, err := getPaymentResult(stream) + msatPerUnit := acceptedQuote.BidPrice + numUnits := uint64(decodedInvoice.NumMsat) / msatPerUnit + t.Logf("Got quote for %v asset units at %v msat/unit from peer %s "+ + "with SCID %d", numUnits, msatPerUnit, peerPubKey, + acceptedQuote.Scid) + + result, err := getAssetPaymentResult(stream) require.NoError(t, err) require.Equal(t, lnrpc.Payment_SUCCEEDED, result.Status) diff --git a/itest/litd_accounts_test.go b/itest/litd_accounts_test.go index f1677c255..781f6a9d0 100644 --- a/itest/litd_accounts_test.go +++ b/itest/litd_accounts_test.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" @@ -433,3 +434,30 @@ func getPaymentResult(stream routerrpc.Router_SendPaymentV2Client) ( } } } + +func getAssetPaymentResult( + s tapchannelrpc.TaprootAssetChannels_SendPaymentClient) (*lnrpc.Payment, + error) { + + // No idea why it makes a difference whether we wait before calling + // s.Recv() or not, but it does. Without the sleep, the test will fail + // with "insufficient local balance"... ¯\_(ツ)_/¯ + // Probably something weird within lnd itself. + time.Sleep(time.Second) + + for { + msg, err := s.Recv() + if err != nil { + return nil, err + } + + payment := msg.GetPaymentResult() + if payment == nil { + return nil, fmt.Errorf("unexpected message: %v", msg) + } + + if payment.Status != lnrpc.Payment_IN_FLIGHT { + return payment, nil + } + } +} diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index 4ba604bcf..c9119c445 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -425,7 +425,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, // Dave, making it possible to send another asset HTLC below, sending // all assets back to Charlie (so we have enough balance for further // tests). - sendKeySendPayment(t.t, charlie, dave, 2000, nil) + sendKeySendPayment(t.t, charlie, dave, 2000) logBalance(t.t, nodes, assetID, "after BTC only keysend") // Let's keysend the rest of the balance back to Charlie. @@ -874,7 +874,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, daveAssetBalance -= keySendAmount // We should also be able to do a non-asset (BTC only) keysend payment. - sendKeySendPayment(t.t, charlie, dave, 2000, nil) + sendKeySendPayment(t.t, charlie, dave, 2000) logBalance(t.t, nodes, assetID, "after BTC only keysend") // ------------ From 8853d2cd86e622646c4a3a45ec58d3429d2c36bf Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 29 Jul 2024 16:57:03 +0200 Subject: [PATCH 3/3] cmd+itest: use new tapchannelrpc.AddInvoice RPC --- cmd/litcli/ln.go | 247 ++++++++----------------------------------- itest/assets_test.go | 92 ++++------------ 2 files changed, 67 insertions(+), 272 deletions(-) diff --git a/cmd/litcli/ln.go b/cmd/litcli/ln.go index 473d43354..3da7f621e 100644 --- a/cmd/litcli/ln.go +++ b/cmd/litcli/ln.go @@ -5,22 +5,18 @@ import ( "context" "crypto/rand" "encoding/hex" - "encoding/json" "errors" "fmt" "strconv" - "time" "github.com/lightninglabs/taproot-assets/asset" - "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/taprpc" - "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightningnetwork/lnd/cmd/commands" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/urfave/cli" "google.golang.org/grpc" @@ -160,64 +156,6 @@ func fundChannel(c *cli.Context) error { return nil } -type assetBalance struct { - AssetID string - Name string - LocalBalance uint64 - RemoteBalance uint64 - Channel *lnrpc.Channel -} - -type channelBalResp struct { - Assets map[string]*assetBalance `json:"assets"` -} - -func computeAssetBalances(lnd lnrpc.LightningClient) (*channelBalResp, error) { - ctxb := context.Background() - openChans, err := lnd.ListChannels( - ctxb, &lnrpc.ListChannelsRequest{}, - ) - if err != nil { - return nil, fmt.Errorf("unable to fetch channels: %w", err) - } - - balanceResp := &channelBalResp{ - Assets: make(map[string]*assetBalance), - } - for _, openChan := range openChans.Channels { - if len(openChan.CustomChannelData) == 0 { - continue - } - - var assetData rfqmsg.JsonAssetChannel - err = json.Unmarshal(openChan.CustomChannelData, &assetData) - if err != nil { - return nil, fmt.Errorf("unable to unmarshal asset "+ - "data: %w", err) - } - - for _, assetOutput := range assetData.Assets { - assetID := assetOutput.AssetInfo.AssetGenesis.AssetID - assetName := assetOutput.AssetInfo.AssetGenesis.Name - - balance, ok := balanceResp.Assets[assetID] - if !ok { - balance = &assetBalance{ - AssetID: assetID, - Name: assetName, - Channel: openChan, - } - balanceResp.Assets[assetID] = balance - } - - balance.LocalBalance += assetOutput.LocalBalance - balance.RemoteBalance += assetOutput.RemoteBalance - } - } - - return balanceResp, nil -} - var ( assetIDFlag = cli.StringFlag{ Name: "asset_id", @@ -517,7 +455,8 @@ func payInvoice(ctx *cli.Context) error { stream, err := tchrpcClient.SendPayment( ctx, &tchrpc.SendPaymentRequest{ - AssetId: assetIDBytes, + AssetId: assetIDBytes, + PaymentRequest: req, }, ) if err != nil { @@ -551,6 +490,14 @@ var addInvoiceCommand = cli.Command{ Name: "asset_amount", Usage: "the amount of assets to receive", }, + cli.StringFlag{ + Name: "rfq_peer_pubkey", + Usage: "(optional) the public key of the peer to ask " + + "for a quote when converting from assets to " + + "sats for the invoice; must be set if there " + + "are multiple channels with the same " + + "asset ID present", + }, ), Action: addInvoice, } @@ -572,6 +519,8 @@ func addInvoice(ctx *cli.Context) error { var ( assetAmount uint64 + preimage []byte + descHash []byte err error ) switch { @@ -587,167 +536,63 @@ func addInvoice(ctx *cli.Context) error { return fmt.Errorf("asset_amount argument missing") } - expiry := time.Now().Add(300 * time.Second) - if ctx.IsSet("expiry") { - expirySeconds := ctx.Uint64("expiry") - expiry = time.Now().Add( - time.Duration(expirySeconds) * time.Second, - ) + if ctx.IsSet("preimage") { + preimage, err = hex.DecodeString(ctx.String("preimage")) + if err != nil { + return fmt.Errorf("unable to parse preimage: %w", err) + } } - lndConn, cleanup, err := connectClient(ctx, false) + descHash, err = hex.DecodeString(ctx.String("description_hash")) if err != nil { - return fmt.Errorf("unable to make rpc con: %w", err) + return fmt.Errorf("unable to parse description_hash: %w", err) } - defer cleanup() - - lndClient := lnrpc.NewLightningClient(lndConn) + expirySeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) + if ctx.IsSet("expiry") { + expirySeconds = ctx.Int64("expiry") + } assetIDBytes, err := hex.DecodeString(assetIDStr) if err != nil { return fmt.Errorf("unable to decode assetID: %v", err) } - // First, based on the asset ID and amount, we'll make sure that this - // channel even has enough funds to send. - assetBalances, err := computeAssetBalances(lndClient) - if err != nil { - return fmt.Errorf("unable to compute asset balances: %w", err) - } - - balance, ok := assetBalances.Assets[assetIDStr] - if !ok { - return fmt.Errorf("unable to send asset_id=%v, not in "+ - "channel", assetIDStr) - } - - if balance.RemoteBalance == 0 { - return fmt.Errorf("no remote asset balance available for "+ - "receiving asset_id=%v", assetIDStr) - } - var assetID asset.ID copy(assetID[:], assetIDBytes) - tapdConn, cleanup, err := connectTapdClient(ctx) - if err != nil { - return fmt.Errorf("error creating tapd connection: %w", err) - } - - defer cleanup() - - peerPubKey, err := hex.DecodeString(balance.Channel.RemotePubkey) - if err != nil { - return fmt.Errorf("unable to decode peer pubkey: %w", err) - } - - rfqClient := rfqrpc.NewRfqClient(tapdConn) - - timeoutSeconds := uint32(60) - fmt.Printf("Asking peer %x for quote to buy assets to receive for "+ - "invoice over %d units; waiting up to %ds\n", peerPubKey, - assetAmount, timeoutSeconds) - - resp, err := rfqClient.AddAssetBuyOrder( - ctxb, &rfqrpc.AddAssetBuyOrderRequest{ - AssetSpecifier: &rfqrpc.AssetSpecifier{ - Id: &rfqrpc.AssetSpecifier_AssetIdStr{ - AssetIdStr: assetIDStr, - }, - }, - MinAssetAmount: assetAmount, - Expiry: uint64(expiry.Unix()), - PeerPubKey: peerPubKey, - TimeoutSeconds: timeoutSeconds, - }, - ) + rfqPeerKey, err := hex.DecodeString(ctx.String(rfqPeerPubKeyFlag.Name)) if err != nil { - return fmt.Errorf("error adding sell order: %w", err) - } - - var acceptedQuote *rfqrpc.PeerAcceptedBuyQuote - switch r := resp.Response.(type) { - case *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote: - acceptedQuote = r.AcceptedQuote - - case *rfqrpc.AddAssetBuyOrderResponse_InvalidQuote: - return fmt.Errorf("peer %v sent back an invalid quote, "+ - "status: %v", r.InvalidQuote.Peer, - r.InvalidQuote.Status.String()) - - case *rfqrpc.AddAssetBuyOrderResponse_RejectedQuote: - return fmt.Errorf("peer %v rejected the quote, code: %v, "+ - "error message: %v", r.RejectedQuote.Peer, - r.RejectedQuote.ErrorCode, r.RejectedQuote.ErrorMessage) - - default: - return fmt.Errorf("unexpected response type: %T", r) + return fmt.Errorf("unable to decode RFQ peer public key: "+ + "%w", err) } - msatPerUnit := acceptedQuote.AskPrice - numMSats := lnwire.MilliSatoshi(assetAmount * msatPerUnit) - - descHash, err := hex.DecodeString(ctx.String("description_hash")) + tapdConn, cleanup, err := connectTapdClient(ctx) if err != nil { - return fmt.Errorf("unable to parse description_hash: %w", err) + return fmt.Errorf("error creating tapd connection: %w", err) } + defer cleanup() - ourPolicy, err := getOurPolicy( - lndClient, balance.Channel.ChanId, balance.Channel.RemotePubkey, - ) - if err != nil { - return fmt.Errorf("unable to get our policy: %w", err) - } - - hopHint := &lnrpc.HopHint{ - NodeId: balance.Channel.RemotePubkey, - ChanId: acceptedQuote.Scid, - FeeBaseMsat: uint32(ourPolicy.FeeBaseMsat), - FeeProportionalMillionths: uint32(ourPolicy.FeeRateMilliMsat), - CltvExpiryDelta: ourPolicy.TimeLockDelta, - } - - invoice := &lnrpc.Invoice{ - Memo: ctx.String("memo"), - ValueMsat: int64(numMSats), - DescriptionHash: descHash, - FallbackAddr: ctx.String("fallback_addr"), - Expiry: int64(ctx.Uint64("expiry")), - Private: ctx.Bool("private"), - IsAmp: ctx.Bool("amp"), - RouteHints: []*lnrpc.RouteHint{ - { - HopHints: []*lnrpc.HopHint{hopHint}, - }, + channelsClient := tchrpc.NewTaprootAssetChannelsClient(tapdConn) + resp, err := channelsClient.AddInvoice(ctxb, &tchrpc.AddInvoiceRequest{ + AssetId: assetIDBytes, + AssetAmount: assetAmount, + PeerPubkey: rfqPeerKey, + InvoiceRequest: &lnrpc.Invoice{ + Memo: ctx.String("memo"), + RPreimage: preimage, + DescriptionHash: descHash, + FallbackAddr: ctx.String("fallback_addr"), + Expiry: expirySeconds, + Private: ctx.Bool("private"), + IsAmp: ctx.Bool("amp"), }, - } - - invoiceResp, err := lndClient.AddInvoice(ctxb, invoice) - if err != nil { - return err - } - - printRespJSON(invoiceResp) - - return nil -} - -func getOurPolicy(lndClient lnrpc.LightningClient, chanID uint64, - remotePubKey string) (*lnrpc.RoutingPolicy, error) { - - ctxb := context.Background() - edge, err := lndClient.GetChanInfo(ctxb, &lnrpc.ChanInfoRequest{ - ChanId: chanID, }) if err != nil { - return nil, fmt.Errorf("unable to fetch channel: %w", err) + return fmt.Errorf("error adding invoice: %w", err) } - policy := edge.Node1Policy - if edge.Node1Pub == remotePubKey { - policy = edge.Node2Policy - } + printRespJSON(resp) - return policy, nil + return nil } diff --git a/itest/assets_test.go b/itest/assets_test.go index 845342ea7..70afbb895 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -9,7 +9,6 @@ import ( "fmt" "os" "testing" - "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -19,6 +18,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/taproot-assets/itest" "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/rfqmsg" "github.com/lightninglabs/taproot-assets/tapchannel" "github.com/lightninglabs/taproot-assets/tapfreighter" @@ -772,91 +772,41 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) defer cancel() - timeoutSeconds := uint32(60) - expiry := time.Now().Add(time.Duration(timeoutSeconds) * time.Second) + timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) t.Logf("Asking peer %x for quote to buy assets to receive for "+ "invoice over %d units; waiting up to %ds", dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds) dstTapd := newTapClient(t, dst) - resp, err := dstTapd.AddAssetBuyOrder( - ctxt, &rfqrpc.AddAssetBuyOrderRequest{ - AssetSpecifier: &rfqrpc.AssetSpecifier{ - Id: &rfqrpc.AssetSpecifier_AssetId{ - AssetId: assetID, - }, - }, - MinAssetAmount: assetAmount, - Expiry: uint64(expiry.Unix()), - PeerPubKey: dstRfqPeer.PubKey[:], - TimeoutSeconds: timeoutSeconds, - }, - ) - require.NoError(t, err) - - var acceptedQuote *rfqrpc.PeerAcceptedBuyQuote - switch r := resp.Response.(type) { - case *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote: - acceptedQuote = r.AcceptedQuote - - case *rfqrpc.AddAssetBuyOrderResponse_InvalidQuote: - t.Fatalf("peer %v sent back an invalid quote, "+ - "status: %v", r.InvalidQuote.Peer, - r.InvalidQuote.Status.String()) - - case *rfqrpc.AddAssetBuyOrderResponse_RejectedQuote: - t.Fatalf("peer %v rejected the quote, code: %v, "+ - "error message: %v", r.RejectedQuote.Peer, - r.RejectedQuote.ErrorCode, r.RejectedQuote.ErrorMessage) - default: - t.Fatalf("unexpected response type: %T", r) - } - - mSatPerUnit := acceptedQuote.AskPrice - numMSats := lnwire.MilliSatoshi(assetAmount * mSatPerUnit) - - t.Logf("Got quote for %d sats at %v msat/unit from peer %x with SCID "+ - "%d", numMSats.ToSatoshis(), mSatPerUnit, dstRfqPeer.PubKey[:], - acceptedQuote.Scid) - - peerChannels, err := dst.ListChannels(ctxt, &lnrpc.ListChannelsRequest{ - Peer: dstRfqPeer.PubKey[:], + resp, err := dstTapd.AddInvoice(ctxt, &tchrpc.AddInvoiceRequest{ + AssetId: assetID, + AssetAmount: assetAmount, + PeerPubkey: dstRfqPeer.PubKey[:], + InvoiceRequest: &lnrpc.Invoice{ + Memo: fmt.Sprintf("this is an asset invoice over "+ + "%d units", assetAmount), + Expiry: timeoutSeconds, + }, }) require.NoError(t, err) - require.Len(t, peerChannels.Channels, 1) - peerChannel := peerChannels.Channels[0] - ourPolicy, err := getOurPolicy( - dst, peerChannel.ChanId, dstRfqPeer.PubKeyStr, - ) + decodedInvoice, err := dst.DecodePayReq(ctxt, &lnrpc.PayReqString{ + PayReq: resp.InvoiceResult.PaymentRequest, + }) require.NoError(t, err) - hopHint := &lnrpc.HopHint{ - NodeId: dstRfqPeer.PubKeyStr, - ChanId: acceptedQuote.Scid, - FeeBaseMsat: uint32(ourPolicy.FeeBaseMsat), - FeeProportionalMillionths: uint32(ourPolicy.FeeRateMilliMsat), - CltvExpiryDelta: ourPolicy.TimeLockDelta, - } + mSatPerUnit := resp.AcceptedBuyQuote.AskPrice + numMSats := lnwire.MilliSatoshi(assetAmount * mSatPerUnit) - invoice := &lnrpc.Invoice{ - Memo: fmt.Sprintf("this is an asset invoice over "+ - "%d units", assetAmount), - ValueMsat: int64(numMSats), - Expiry: int64(timeoutSeconds), - RouteHints: []*lnrpc.RouteHint{ - { - HopHints: []*lnrpc.HopHint{hopHint}, - }, - }, - } + require.EqualValues(t, numMSats, decodedInvoice.NumMsat) - invoiceResp, err := dst.AddInvoice(ctxb, invoice) - require.NoError(t, err) + t.Logf("Got quote for %d sats at %v msat/unit from peer %x with SCID "+ + "%d", decodedInvoice.NumMsat, mSatPerUnit, dstRfqPeer.PubKey[:], + resp.AcceptedBuyQuote.Scid) - return invoiceResp + return resp.InvoiceResult } func waitForSendEvent(t *testing.T,