From 09b25a830f2dd3a51b41a561603a8d6a6ff69859 Mon Sep 17 00:00:00 2001 From: Buddhi Thilakshana Date: Sun, 13 Sep 2026 20:12:57 +0530 Subject: [PATCH] fix: accept lnbits 1.x RFC3339 payment timestamps lnbits 0.x serialises a payment's `time` as unix seconds and 1.x as an RFC3339 string, so moving the backend to 1.x broke transaction history with "cannot unmarshal string into Go struct field Payment.time of type int". Decode into a UnixTime that takes either form and normalises to seconds. Marshalling still emits the number, so the API responses these types are serialised into are unchanged. The webhook payload carries the same field. Nothing had failed there yet only because no deposit had arrived since the migration - a decode error on that path drops the incoming payment notification. --- internal/api/analytics.go | 4 ++-- internal/lnbits/types.go | 34 +++++++++++++++++++++++++++++- internal/lnbits/webhook/webhook.go | 26 +++++++++++------------ 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/internal/api/analytics.go b/internal/api/analytics.go index 58792d9e..ee0e1c2c 100644 --- a/internal/api/analytics.go +++ b/internal/api/analytics.go @@ -272,7 +272,7 @@ func (s Service) GetTransactionAnalytics(w http.ResponseWriter, r *http.Request) Fee: payment.Fee, FeeSats: payment.Fee / 1000, Memo: payment.Memo, - Time: payment.Time, + Time: int(payment.Time), Timestamp: paymentTime.Format(time.RFC3339), PaymentType: paymentType, Bolt11: payment.Bolt11, @@ -443,7 +443,7 @@ func (s Service) GetUserTransactionHistory(w http.ResponseWriter, r *http.Reques Fee: payment.Fee, FeeSats: payment.Fee / 1000, Memo: payment.Memo, - Time: payment.Time, + Time: int(payment.Time), Timestamp: paymentTime.Format(time.RFC3339), PaymentType: paymentType, Bolt11: payment.Bolt11, diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 6cdb15e3..3ded6c4d 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -3,6 +3,7 @@ package lnbits import ( "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "time" @@ -142,13 +143,44 @@ type Wallet struct { User string `json:"user"` } +// UnixTime is a payment timestamp in unix seconds. lnbits 0.x serialises +// `time` as a number, 1.x as an RFC3339 string, so decoding accepts either and +// normalises to seconds. Encoding always emits the number, keeping the API +// responses this type is serialised into unchanged. +type UnixTime int64 + +func (t *UnixTime) UnmarshalJSON(b []byte) error { + s := string(b) + if s == "null" || s == `""` { + return nil + } + if b[0] != '"' { + var n int64 + if err := json.Unmarshal(b, &n); err != nil { + return err + } + *t = UnixTime(n) + return nil + } + var str string + if err := json.Unmarshal(b, &str); err != nil { + return err + } + parsed, err := time.Parse(time.RFC3339, str) + if err != nil { + return fmt.Errorf("lnbits: unrecognised payment time %q: %w", str, err) + } + *t = UnixTime(parsed.Unix()) + return nil +} + type Payment struct { CheckingID string `json:"checking_id"` Pending bool `json:"pending"` Amount int64 `json:"amount"` Fee int64 `json:"fee"` Memo string `json:"memo"` - Time int `json:"time"` + Time UnixTime `json:"time"` Bolt11 string `json:"bolt11"` Preimage string `json:"preimage"` PaymentHash string `json:"payment_hash"` diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index 4b457ad3..2030e2e4 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -33,19 +33,19 @@ type Server struct { } type Webhook struct { - CheckingID string `json:"checking_id"` - Pending bool `json:"pending"` - Amount int64 `json:"amount"` - Fee int64 `json:"fee"` - Memo string `json:"memo"` - Time int64 `json:"time"` - Bolt11 string `json:"bolt11"` - Preimage string `json:"preimage"` - PaymentHash string `json:"payment_hash"` - Extra struct{} `json:"extra"` - WalletID string `json:"wallet_id"` - Webhook string `json:"webhook"` - WebhookStatus interface{} `json:"webhook_status"` + CheckingID string `json:"checking_id"` + Pending bool `json:"pending"` + Amount int64 `json:"amount"` + Fee int64 `json:"fee"` + Memo string `json:"memo"` + Time lnbits.UnixTime `json:"time"` + Bolt11 string `json:"bolt11"` + Preimage string `json:"preimage"` + PaymentHash string `json:"payment_hash"` + Extra struct{} `json:"extra"` + WalletID string `json:"wallet_id"` + Webhook string `json:"webhook"` + WebhookStatus interface{} `json:"webhook_status"` } func NewServer(bot *telegram.TipBot) *Server {