From 17a278adc23c812455c2ef8ea90daf840f554f2c Mon Sep 17 00:00:00 2001 From: Delleonmcglone Date: Tue, 14 Jul 2026 16:10:04 -0400 Subject: [PATCH] l402: pay challenge invoices via router's SendPaymentV2 The client interceptor paid L402 challenge invoices through lndclient's LightningClient.PayInvoice, which uses lnd's legacy SendPaymentSync RPC. That RPC is deprecated in lnd and payments made through it can fail to initiate on recent lnd builds: the interceptor then times out waiting, leaving a pending token that every subsequent call tries (and fails) to resume with 'payment isn't initiated', until the token file is removed manually. Dispatch the payment through the router subserver instead (RouterClient.SendPayment -> SendPaymentV2) and consume status updates until a terminal state, mirroring the existing trackPayment flow. The router client is already part of the LndServices the interceptor holds, and the test mock already implements RouterClient.SendPayment, so the interceptor tests move from the SendPaymentChannel to the RouterSendPaymentChannel and deliver PaymentStatus updates instead of a PaymentResult. Observed in the wild running poold 0.7.1 against lnd 0.21.0-beta on testnet: InitAccount consistently failed with 'payment timed out' / 'payment isn't initiated' while lnd showed no payment attempt at all; paying the same invoice via SendPaymentV2 succeeded in about a second. Co-Authored-By: Claude Fable 5 --- l402/client_interceptor.go | 66 ++++++++++++++++++++++----------- l402/client_interceptor_test.go | 26 +++++++------ 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/l402/client_interceptor.go b/l402/client_interceptor.go index bc322719..84352a73 100644 --- a/l402/client_interceptor.go +++ b/l402/client_interceptor.go @@ -388,31 +388,55 @@ func (i *ClientInterceptor) payL402Token(ctx context.Context, md *metadata.MD) ( } // Pay invoice now and wait for the result to arrive or the main context - // being canceled. + // being canceled. The payment is dispatched through lnd's router + // subserver (SendPaymentV2), as the legacy SendPaymentSync RPC that + // was used previously is deprecated and payments made through it can + // fail to initiate on recent lnd versions. payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout) defer cancel() - respChan := i.lnd.Client.PayInvoice( - payCtx, invoiceStr, i.maxFee, nil, + payStatusChan, payErrChan, err := i.lnd.Router.SendPayment( + payCtx, lndclient.SendPaymentRequest{ + Invoice: invoiceStr, + MaxFee: i.maxFee, + Timeout: PaymentTimeout, + }, ) - select { - case result := <-respChan: - if result.Err != nil { - return nil, result.Err + if err != nil { + return nil, fmt.Errorf("unable to dispatch payment: %v", err) + } + + for { + select { + case result := <-payStatusChan: + switch result.State { + // The payment was successful, we have all the + // information we need and can return the fully paid + // token. + case lnrpc.Payment_SUCCEEDED: + token.Preimage = result.Preimage + token.AmountPaid = result.Value + token.RoutingFeePaid = result.Fee + return token, i.store.StoreToken(token) + + case lnrpc.Payment_FAILED: + return nil, fmt.Errorf("payment failed: %v", + result.FailureReason) + + // Any other state means the payment is still in + // flight, we keep waiting for a terminal update. + } + + case err := <-payErrChan: + return nil, fmt.Errorf("error paying invoice: %v", err) + + case <-payCtx.Done(): + return nil, fmt.Errorf("payment timed out. try again "+ + "to track payment. %s", manualRetryHint) + + case <-ctx.Done(): + return nil, fmt.Errorf("parent context canceled. try "+ + "again to track payment. %s", manualRetryHint) } - token.Preimage = result.Preimage - token.AmountPaid = lnwire.NewMSatFromSatoshis(result.PaidAmt) - token.RoutingFeePaid = lnwire.NewMSatFromSatoshis( - result.PaidFee, - ) - return token, i.store.StoreToken(token) - - case <-payCtx.Done(): - return nil, fmt.Errorf("payment timed out. try again to track "+ - "payment. %s", manualRetryHint) - - case <-ctx.Done(): - return nil, fmt.Errorf("parent context canceled. try again to"+ - "track payment. %s", manualRetryHint) } } diff --git a/l402/client_interceptor_test.go b/l402/client_interceptor_test.go index 1eb91cb4..c002eb7c 100644 --- a/l402/client_interceptor_test.go +++ b/l402/client_interceptor_test.go @@ -26,7 +26,7 @@ type interceptTestCase struct { resetCb func(addL402 bool) expectLndCall bool expectSecondLndCall bool - sendPaymentCb func(*testing.T, test.PaymentChannelMessage) + sendPaymentCb func(*testing.T, test.RouterPaymentChannelMessage) trackPaymentCb func(*testing.T, test.TrackPaymentMessage) expectToken bool expectInterceptErr string @@ -103,17 +103,18 @@ var ( }, expectLndCall: true, sendPaymentCb: func(t *testing.T, - msg test.PaymentChannelMessage) { + msg test.RouterPaymentChannelMessage) { require.Len(t, callMD, 0) // The next call to the "backend" shouldn't return an // error. resetBackend(nil, []string{}) - msg.Done <- lndclient.PaymentResult{ + msg.Updates <- lndclient.PaymentStatus{ + State: lnrpc.Payment_SUCCEEDED, Preimage: paidPreimage, - PaidAmt: 123, - PaidFee: 345, + Value: 123_000, + Fee: 345_000, } }, trackPaymentCb: func(t *testing.T, @@ -149,7 +150,7 @@ var ( }, expectLndCall: true, sendPaymentCb: func(t *testing.T, - msg test.PaymentChannelMessage) { + msg test.RouterPaymentChannelMessage) { t.Fatal("didn't expect call to sendPayment") }, @@ -181,17 +182,18 @@ var ( expectLndCall: true, expectSecondLndCall: true, sendPaymentCb: func(t *testing.T, - msg test.PaymentChannelMessage) { + msg test.RouterPaymentChannelMessage) { require.Len(t, callMD, 0) // The next call to the "backend" shouldn't return an // error. resetBackend(nil, []string{}) - msg.Done <- lndclient.PaymentResult{ + msg.Updates <- lndclient.PaymentStatus{ + State: lnrpc.Payment_SUCCEEDED, Preimage: paidPreimage, - PaidAmt: 123, - PaidFee: 345, + Value: 123_000, + Fee: 345_000, } }, trackPaymentCb: func(t *testing.T, @@ -361,7 +363,7 @@ func testInterceptor(t *testing.T, tc interceptTestCase, addL402 bool, // Simulate payment related calls to lnd, if there are any expected. if tc.expectLndCall { select { - case payment := <-lnd.SendPaymentChannel: + case payment := <-lnd.RouterSendPaymentChannel: tc.sendPaymentCb(t, payment) case track := <-lnd.TrackPaymentChannel: @@ -373,7 +375,7 @@ func testInterceptor(t *testing.T, tc interceptTestCase, addL402 bool, } if tc.expectSecondLndCall { select { - case payment := <-lnd.SendPaymentChannel: + case payment := <-lnd.RouterSendPaymentChannel: tc.sendPaymentCb(t, payment) case track := <-lnd.TrackPaymentChannel: