staticaddr/loopin: factor invoice update handling

Extract the monitor invoice update semantics into a helper and cover the existing state mapping with a dedicated test.
This commit is contained in:
Slyghtning 2026-07-07 09:34:48 +02:00
parent ee5d84b323
commit a6d061c568
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
2 changed files with 110 additions and 22 deletions

View file

@ -359,6 +359,35 @@ func (f *FSM) cancelSwapInvoice() {
}
}
// handleInvoiceUpdate applies the monitor state's invoice-update semantics and
// reports whether the update produced a terminal event.
func (f *FSM) handleInvoiceUpdate(update lndclient.InvoiceUpdate) (
fsm.EventType, bool) {
switch update.State {
case invoices.ContractOpen:
return fsm.NoOp, false
case invoices.ContractAccepted:
return fsm.NoOp, false
case invoices.ContractSettled:
f.Debugf("received off-chain payment update %v", update.State)
return OnPaymentReceived, true
case invoices.ContractCanceled:
// If the invoice was canceled we only log here since we still need
// to monitor until the htlc timed out.
log.Warnf("invoice for swap hash %v canceled", f.loopIn.SwapHash)
return fsm.NoOp, false
default:
err := fmt.Errorf("unexpected invoice state %v for swap hash %v "+
"canceled", update.State, f.loopIn.SwapHash)
return f.HandleError(err), true
}
}
// SignHtlcTxAction is called if the htlc was initialized and the server
// provided the necessary information to construct the htlc tx. We sign the htlc
// tx and send the signatures to the server.
@ -737,28 +766,8 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
return f.HandleError(err)
case update := <-invoiceUpdateChan:
switch update.State {
case invoices.ContractOpen:
case invoices.ContractAccepted:
case invoices.ContractSettled:
f.Debugf("received off-chain payment update "+
"%v", update.State)
return OnPaymentReceived
case invoices.ContractCanceled:
// If the invoice was canceled we only log here
// since we still need to monitor until the htlc
// timed out.
log.Warnf("invoice for swap hash %v canceled",
f.loopIn.SwapHash)
default:
err = fmt.Errorf("unexpected invoice state %v "+
"for swap hash %v canceled",
update.State, f.loopIn.SwapHash)
return f.HandleError(err)
if event, done := f.handleInvoiceUpdate(update); done {
return event
}
case err = <-invoiceErrChan:

View file

@ -3,6 +3,7 @@ package loopin
import (
"context"
"errors"
"fmt"
"testing"
"time"
@ -24,6 +25,84 @@ import (
"google.golang.org/grpc"
)
// TestHandleInvoiceUpdate verifies that invoice state updates map to the
// monitor events expected by the static address loop-in FSM.
func TestHandleInvoiceUpdate(t *testing.T) {
t.Parallel()
swapHash := lntypes.Hash{1, 2, 3}
tests := []struct {
name string
state invoices.ContractState
event fsm.EventType
done bool
errString string
}{
{
name: "open",
state: invoices.ContractOpen,
event: fsm.NoOp,
},
{
name: "accepted",
state: invoices.ContractAccepted,
event: fsm.NoOp,
},
{
name: "settled",
state: invoices.ContractSettled,
event: OnPaymentReceived,
done: true,
},
{
name: "canceled",
state: invoices.ContractCanceled,
event: fsm.NoOp,
},
{
name: "unexpected",
state: invoices.ContractState(99),
event: fsm.OnError,
done: true,
errString: "unexpected invoice state",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
f := &FSM{
StateMachine: &fsm.StateMachine{},
loopIn: &StaticAddressLoopIn{
SwapHash: swapHash,
},
}
event, done := f.handleInvoiceUpdate(
lndclient.InvoiceUpdate{
Invoice: lndclient.Invoice{
State: test.state,
},
},
)
require.Equal(t, test.event, event)
require.Equal(t, test.done, done)
if test.errString == "" {
require.Nil(t, f.LastActionError)
} else {
require.ErrorContains(
t, f.LastActionError, test.errString,
)
require.ErrorContains(
t, f.LastActionError, fmt.Sprint(swapHash),
)
}
})
}
}
// TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr ensures that an error from
// the HTLC confirmation subscription triggers a re-registration. Without the
// regression fix, only the initial registration would be performed and the