fix: fallback to outgoing payments in Phoenixd LookupInvoice (#2447)

* fix: fallback to outgoing payments in Phoenixd LookupInvoice

LookupInvoice only queried /payments/incoming/{hash}, returning 404 for
outgoing payments. This caused all outgoing Lightning payments to remain
permanently stuck as PENDING in Alby Hub.

The fix tries incoming first (preserving existing behavior), then falls
back to listing outgoing payments and matching by paymentHash.

Fixes #2442

* fix: amount and fees in phoenix payment to transaction

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
Josip 2026-08-06 12:24:10 +02:00 committed by GitHub
parent fbaff5d8a0
commit 3b3e784fa6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -21,6 +21,9 @@ import (
"github.com/sirupsen/logrus"
)
// errNotFound indicates that a payment was not found at the queried endpoint.
var errNotFound = errors.New("phoenixd: payment not found")
type InvoiceResponse struct {
PaymentHash string `json:"paymentHash"`
Preimage string `json:"preimage"`
@ -259,42 +262,85 @@ func (svc *PhoenixService) CancelHoldInvoice(ctx context.Context, paymentHash st
return errors.New("not implemented")
}
// LookupInvoice looks up a transaction by payment hash. It first checks
// incoming payments, then falls back to outgoing payments if the incoming
// payment is not found (HTTP 404).
func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
transaction, err = svc.lookupIncomingPayment(ctx, paymentHash)
if err == nil {
return transaction, nil
}
// Only fall back to outgoing lookup when incoming returns not-found.
if !errors.Is(err, errNotFound) {
return nil, err
}
return svc.lookupOutgoingPayment(ctx, paymentHash)
}
// lookupIncomingPayment fetches an incoming payment from Phoenixd by payment hash.
func (svc *PhoenixService) lookupIncomingPayment(ctx context.Context, paymentHash string) (*lnclient.Transaction, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.Address+"/payments/incoming/"+paymentHash, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("create phoenixd incoming payment request: %w", err)
}
req.Header.Add("Authorization", "Basic "+svc.Authorization)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("call phoenixd incoming payment endpoint: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
return nil, fmt.Errorf("read phoenixd incoming payment response: %w", err)
}
if resp.StatusCode == http.StatusNotFound {
return nil, errNotFound
}
if resp.StatusCode != http.StatusOK {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"status_code": resp.StatusCode,
}).Error("phoenixd incoming payments endpoint returned non-success code")
return nil, fmt.Errorf("phoenixd incoming payments endpoint returned non-success code: %d %s", resp.StatusCode, string(body))
}
var invoiceRes InvoiceResponse
if err := json.Unmarshal(body, &invoiceRes); err != nil {
return nil, err
return nil, fmt.Errorf("decode phoenixd incoming payment response: %w", err)
}
transaction, err = phoenixInvoiceToTransaction(&invoiceRes)
return phoenixInvoiceToTransaction(&invoiceRes)
}
// lookupOutgoingPayment fetches an outgoing payment from Phoenixd using the
// /payments/outgoingbyhash/{paymentHash} endpoint.
func (svc *PhoenixService) lookupOutgoingPayment(ctx context.Context, paymentHash string) (*lnclient.Transaction, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.Address+"/payments/outgoingbyhash/"+paymentHash, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("create phoenixd outgoing payment request: %w", err)
}
req.Header.Add("Authorization", "Basic "+svc.Authorization)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("call phoenixd outgoing payment endpoint: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read phoenixd outgoing payment response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("phoenixd outgoing payment lookup returned non-success code: %d %s", resp.StatusCode, string(body))
}
return transaction, nil
var paymentRes OutgoingPaymentResponse
if err := json.Unmarshal(body, &paymentRes); err != nil {
return nil, fmt.Errorf("decode phoenixd outgoing payment response: %w", err)
}
return outgoingPaymentToTransaction(&paymentRes)
}
func (svc *PhoenixService) SendPaymentSync(payReq string, amountMsat *uint64) (*lnclient.PayInvoiceResponse, error) {
@ -488,6 +534,47 @@ func phoenixInvoiceToTransaction(invoiceRes *InvoiceResponse) (*lnclient.Transac
}, nil
}
// outgoingPaymentToTransaction converts a Phoenixd OutgoingPaymentResponse
// to an lnclient.Transaction.
func outgoingPaymentToTransaction(payment *OutgoingPaymentResponse) (*lnclient.Transaction, error) {
var settledAt *int64
if payment.CompletedAt != 0 {
settledAtUnix := time.UnixMilli(payment.CompletedAt).Unix()
settledAt = &settledAtUnix
}
paymentRequest, err := decodepay.Decodepay(payment.Invoice)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"bolt11": payment.Invoice,
}).Errorf("Failed to decode bolt11 invoice: %v", err)
return nil, fmt.Errorf("decode phoenixd outgoing payment bolt11: %w", err)
}
expiresAt := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
// unlike incoming payments, "fees" on outgoing payments is in millisats,
// and "sent" (in sats) includes the fees
amountMsat := paymentRequest.MSatoshi
if amountMsat == 0 {
amountMsat = payment.Sent*1000 - payment.Fees
}
return &lnclient.Transaction{
Type: "outgoing",
Invoice: payment.Invoice,
Preimage: payment.Preimage,
PaymentHash: payment.PaymentHash,
AmountMsat: amountMsat,
FeesPaidMsat: payment.Fees,
CreatedAt: time.UnixMilli(payment.CreatedAt).Unix(),
Description: paymentRequest.Description,
SettledAt: settledAt,
ExpiresAt: &expiresAt,
DescriptionHash: paymentRequest.DescriptionHash,
}, nil
}
func (svc *PhoenixService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
return nil
}