mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
- Replace occurrences of `// nolint:lll` with `// nolint:ll` across files for consistency. - Reformat multiline strings, comments, and function parameters to improve clarity and adhere to style guidelines. - Add `// nolint:ll` comments where necessary to prevent linter warnings.
1115 lines
30 KiB
Go
1115 lines
30 KiB
Go
package accounts
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/lightningnetwork/lnd/clock"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
var (
|
|
chainParams = &chaincfg.RegressionNetParams
|
|
|
|
marshalOptions = &protojson.MarshalOptions{
|
|
UseProtoNames: true,
|
|
EmitUnpopulated: true,
|
|
}
|
|
|
|
testID = AccountID{77, 88, 99}
|
|
testHash = lntypes.Hash{
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
}
|
|
testHash2 = lntypes.Hash{
|
|
2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
}
|
|
testHash3 = lntypes.Hash{
|
|
3, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
}
|
|
testHash4 = lntypes.Hash{
|
|
4, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
}
|
|
|
|
testAmount = &lnrpc.Amount{
|
|
Sat: 456,
|
|
Msat: 456789,
|
|
}
|
|
emptyAmount = &lnrpc.Amount{
|
|
Sat: 0,
|
|
Msat: 0,
|
|
}
|
|
)
|
|
|
|
type mockService struct {
|
|
acctBalanceMsat lnwire.MilliSatoshi
|
|
|
|
trackedInvoices map[lntypes.Hash]AccountID
|
|
trackedPayments AccountPayments
|
|
|
|
*requestValuesStore
|
|
}
|
|
|
|
func (m *mockService) CreditAccount(_ context.Context, _ AccountID,
|
|
_ lnwire.MilliSatoshi) (*OffChainBalanceAccount, error) {
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockService) DebitAccount(_ context.Context, _ AccountID,
|
|
_ lnwire.MilliSatoshi) (*OffChainBalanceAccount, error) {
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func newMockService() *mockService {
|
|
return &mockService{
|
|
acctBalanceMsat: 0,
|
|
trackedInvoices: make(map[lntypes.Hash]AccountID),
|
|
trackedPayments: make(AccountPayments),
|
|
requestValuesStore: newRequestValuesStore(),
|
|
}
|
|
}
|
|
|
|
func (m *mockService) CheckBalance(_ context.Context, _ AccountID,
|
|
wantBalance lnwire.MilliSatoshi) error {
|
|
|
|
if wantBalance > m.acctBalanceMsat {
|
|
return fmt.Errorf("invalid balance")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *mockService) AssociateInvoice(_ context.Context, id AccountID,
|
|
hash lntypes.Hash) error {
|
|
|
|
m.trackedInvoices[hash] = id
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *mockService) AssociatePayment(_ context.Context, id AccountID,
|
|
paymentHash lntypes.Hash, amt lnwire.MilliSatoshi) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *mockService) PaymentErrored(_ context.Context, id AccountID,
|
|
hash lntypes.Hash) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *mockService) TrackPayment(_ context.Context, _ AccountID,
|
|
hash lntypes.Hash, amt lnwire.MilliSatoshi) error {
|
|
|
|
m.trackedPayments[hash] = &PaymentEntry{
|
|
Status: lnrpc.Payment_UNKNOWN,
|
|
FullAmount: amt,
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *mockService) RemovePayment(_ context.Context,
|
|
hash lntypes.Hash) error {
|
|
|
|
delete(m.trackedPayments, hash)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (*mockService) IsRunning() bool {
|
|
return true
|
|
}
|
|
|
|
var _ Service = (*mockService)(nil)
|
|
|
|
// TestAccountChecker makes sure all round trip checkers can be instantiated
|
|
// correctly without panicking.
|
|
func TestAccountChecker(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checker := NewAccountChecker(nil, nil)
|
|
for checkerName := range checker.checkers {
|
|
t.Logf("Checker registered: %v", checkerName)
|
|
}
|
|
}
|
|
|
|
// TestAccountCheckers tests the account request checkers.
|
|
func TestAccountCheckers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const reqID = uint64(55)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
fullURI string
|
|
setup func(s *mockService,
|
|
acct *OffChainBalanceAccount)
|
|
originalRequest proto.Message
|
|
requestErr string
|
|
originalResponse proto.Message
|
|
replacedResponse proto.Message
|
|
responseErr string
|
|
validate func(t *testing.T, s *mockService,
|
|
acct *OffChainBalanceAccount)
|
|
}{{
|
|
name: "node info pass through",
|
|
fullURI: "/lnrpc.Lightning/GetNodeInfo",
|
|
originalRequest: &lnrpc.NodeInfoRequest{
|
|
PubKey: "foobarbaz",
|
|
},
|
|
originalResponse: &lnrpc.NodeInfo{
|
|
Node: &lnrpc.LightningNode{
|
|
LastUpdate: 1234,
|
|
PubKey: "foobarbaz",
|
|
Alias: "baz",
|
|
Color: "green",
|
|
},
|
|
},
|
|
}, {
|
|
name: "add invoice",
|
|
fullURI: "/lnrpc.Lightning/AddInvoice",
|
|
originalRequest: &lnrpc.Invoice{},
|
|
originalResponse: &lnrpc.AddInvoiceResponse{
|
|
RHash: testHash[:],
|
|
},
|
|
validate: func(t *testing.T, s *mockService,
|
|
acct *OffChainBalanceAccount) {
|
|
|
|
require.Contains(t, s.trackedInvoices, testHash)
|
|
},
|
|
}, {
|
|
name: "list invoices, not mapped to account",
|
|
fullURI: "/lnrpc.Lightning/ListInvoices",
|
|
originalRequest: &lnrpc.ListInvoiceRequest{},
|
|
originalResponse: &lnrpc.ListInvoiceResponse{
|
|
Invoices: []*lnrpc.Invoice{{
|
|
RHash: testHash[:],
|
|
}},
|
|
},
|
|
replacedResponse: &lnrpc.ListInvoiceResponse{
|
|
Invoices: []*lnrpc.Invoice{},
|
|
},
|
|
}, {
|
|
name: "list invoices, mapped to account",
|
|
fullURI: "/lnrpc.Lightning/ListInvoices",
|
|
setup: func(s *mockService, acct *OffChainBalanceAccount) {
|
|
acct.Invoices[testHash] = struct{}{}
|
|
},
|
|
originalRequest: &lnrpc.ListInvoiceRequest{},
|
|
originalResponse: &lnrpc.ListInvoiceResponse{
|
|
Invoices: []*lnrpc.Invoice{{
|
|
RHash: testHash[:],
|
|
}},
|
|
},
|
|
replacedResponse: &lnrpc.ListInvoiceResponse{
|
|
Invoices: []*lnrpc.Invoice{{
|
|
RHash: testHash[:],
|
|
}},
|
|
},
|
|
}, {
|
|
name: "lookup invoice, not mapped to account",
|
|
fullURI: "/lnrpc.Lightning/LookupInvoice",
|
|
originalRequest: &lnrpc.PaymentHash{
|
|
RHash: testHash[:],
|
|
},
|
|
requestErr: "invoice does not belong to this account",
|
|
}, {
|
|
name: "lookup invoice, mapped to account",
|
|
fullURI: "/lnrpc.Lightning/LookupInvoice",
|
|
setup: func(s *mockService, acct *OffChainBalanceAccount) {
|
|
acct.Invoices[testHash] = struct{}{}
|
|
},
|
|
originalRequest: &lnrpc.PaymentHash{
|
|
RHash: testHash[:],
|
|
},
|
|
originalResponse: &lnrpc.Invoice{
|
|
RHash: testHash[:],
|
|
},
|
|
}, {
|
|
name: "send payment, not enough balance",
|
|
fullURI: "/lnrpc.Lightning/SendPaymentSync",
|
|
originalRequest: &lnrpc.SendRequest{
|
|
AmtMsat: 5000,
|
|
PaymentHash: testHash[:],
|
|
},
|
|
requestErr: "error validating account balance: invalid balance",
|
|
}, {
|
|
name: "send payment, not enough balance because of fee",
|
|
fullURI: "/lnrpc.Lightning/SendPaymentSync",
|
|
setup: func(s *mockService, acct *OffChainBalanceAccount) {
|
|
s.acctBalanceMsat = 5000
|
|
},
|
|
originalRequest: &lnrpc.SendRequest{
|
|
AmtMsat: 5000,
|
|
FeeLimit: &lnrpc.FeeLimit{
|
|
Limit: &lnrpc.FeeLimit_Percent{
|
|
Percent: 1,
|
|
},
|
|
},
|
|
PaymentHash: testHash[:],
|
|
},
|
|
requestErr: "error validating account balance: invalid balance",
|
|
}, {
|
|
name: "send payment, exact balance",
|
|
fullURI: "/lnrpc.Lightning/SendPaymentSync",
|
|
setup: func(s *mockService, acct *OffChainBalanceAccount) {
|
|
s.acctBalanceMsat = 5123
|
|
},
|
|
originalRequest: &lnrpc.SendRequest{
|
|
AmtMsat: 5000,
|
|
FeeLimit: &lnrpc.FeeLimit{
|
|
Limit: &lnrpc.FeeLimit_FixedMsat{
|
|
FixedMsat: 123,
|
|
},
|
|
},
|
|
PaymentHash: testHash[:],
|
|
},
|
|
originalResponse: &lnrpc.SendResponse{
|
|
PaymentHash: testHash[:],
|
|
PaymentRoute: &lnrpc.Route{
|
|
TotalAmtMsat: 5000,
|
|
TotalFeesMsat: 123,
|
|
},
|
|
},
|
|
validate: func(t *testing.T, s *mockService,
|
|
acct *OffChainBalanceAccount) {
|
|
|
|
require.Contains(t, s.trackedPayments, testHash)
|
|
payment := s.trackedPayments[testHash]
|
|
require.EqualValues(t, 5123, payment.FullAmount)
|
|
|
|
// We start tracking the payment and don't look at the
|
|
// payment state reported by the response.
|
|
require.Equal(
|
|
t, lnrpc.Payment_UNKNOWN, payment.Status,
|
|
)
|
|
},
|
|
}, {
|
|
name: "list payments, not mapped to account",
|
|
fullURI: "/lnrpc.Lightning/ListPayments",
|
|
originalRequest: &lnrpc.ListPaymentsRequest{},
|
|
originalResponse: &lnrpc.ListPaymentsResponse{
|
|
Payments: []*lnrpc.Payment{{
|
|
PaymentHash: hex.EncodeToString(testHash[:]),
|
|
}},
|
|
},
|
|
replacedResponse: &lnrpc.ListPaymentsResponse{
|
|
Payments: []*lnrpc.Payment{},
|
|
},
|
|
}, {
|
|
name: "list payments, mapped to account",
|
|
fullURI: "/lnrpc.Lightning/ListPayments",
|
|
setup: func(s *mockService, acct *OffChainBalanceAccount) {
|
|
acct.Payments[testHash] = &PaymentEntry{}
|
|
},
|
|
originalRequest: &lnrpc.ListPaymentsRequest{},
|
|
originalResponse: &lnrpc.ListPaymentsResponse{
|
|
Payments: []*lnrpc.Payment{{
|
|
PaymentHash: hex.EncodeToString(testHash[:]),
|
|
}},
|
|
},
|
|
replacedResponse: &lnrpc.ListPaymentsResponse{
|
|
Payments: []*lnrpc.Payment{{
|
|
PaymentHash: hex.EncodeToString(testHash[:]),
|
|
}},
|
|
},
|
|
}, {
|
|
name: "track payment, not mapped to account",
|
|
fullURI: "/routerrpc.Router/TrackPaymentV2",
|
|
originalRequest: &routerrpc.TrackPaymentRequest{
|
|
PaymentHash: testHash[:],
|
|
},
|
|
requestErr: "payment does not belong to this account",
|
|
}, {
|
|
name: "track payment, mapped to account",
|
|
fullURI: "/routerrpc.Router/TrackPaymentV2",
|
|
setup: func(s *mockService, acct *OffChainBalanceAccount) {
|
|
acct.Payments[testHash] = &PaymentEntry{}
|
|
},
|
|
originalRequest: &routerrpc.TrackPaymentRequest{
|
|
PaymentHash: testHash[:],
|
|
},
|
|
originalResponse: &lnrpc.Payment{
|
|
PaymentHash: hex.EncodeToString(testHash[:]),
|
|
},
|
|
}, {
|
|
name: "deprecated: router send payment v1",
|
|
fullURI: "/routerrpc.Router/SendPayment",
|
|
originalRequest: &routerrpc.SendPaymentRequest{
|
|
PaymentHash: testHash[:],
|
|
},
|
|
requestErr: "this RPC call is not supported with restricted " +
|
|
"account macaroons",
|
|
}, {
|
|
name: "deprecated: router send to route v1",
|
|
fullURI: "/routerrpc.Router/SendToRoute",
|
|
originalRequest: &routerrpc.SendToRouteRequest{
|
|
PaymentHash: testHash[:],
|
|
},
|
|
requestErr: "this RPC call is not supported with restricted " +
|
|
"account macaroons",
|
|
}, {
|
|
name: "deprecated: router track payment v1",
|
|
fullURI: "/routerrpc.Router/TrackPayment",
|
|
originalRequest: &routerrpc.TrackPaymentRequest{
|
|
PaymentHash: testHash[:],
|
|
},
|
|
requestErr: "this RPC call is not supported with restricted " +
|
|
"account macaroons",
|
|
}, {
|
|
name: "empty response: pending channels",
|
|
fullURI: "/lnrpc.Lightning/PendingChannels",
|
|
originalRequest: &lnrpc.PendingChannelsRequest{},
|
|
originalResponse: &lnrpc.PendingChannelsResponse{
|
|
TotalLimboBalance: 123456,
|
|
// nolint:ll
|
|
PendingOpenChannels: []*lnrpc.PendingChannelsResponse_PendingOpenChannel{
|
|
{},
|
|
},
|
|
},
|
|
replacedResponse: &lnrpc.PendingChannelsResponse{},
|
|
}, {
|
|
name: "empty response: list channels",
|
|
fullURI: "/lnrpc.Lightning/ListChannels",
|
|
originalRequest: &lnrpc.ListChannelsRequest{},
|
|
originalResponse: &lnrpc.ListChannelsResponse{
|
|
Channels: []*lnrpc.Channel{
|
|
{},
|
|
},
|
|
},
|
|
replacedResponse: &lnrpc.ListChannelsResponse{},
|
|
}, {
|
|
name: "empty response: closed channels",
|
|
fullURI: "/lnrpc.Lightning/ClosedChannels",
|
|
originalRequest: &lnrpc.ClosedChannelsRequest{},
|
|
originalResponse: &lnrpc.ClosedChannelsResponse{
|
|
Channels: []*lnrpc.ChannelCloseSummary{
|
|
{},
|
|
},
|
|
},
|
|
replacedResponse: &lnrpc.ClosedChannelsResponse{},
|
|
}, {
|
|
name: "channel balance",
|
|
fullURI: "/lnrpc.Lightning/ChannelBalance",
|
|
setup: func(s *mockService, acct *OffChainBalanceAccount) {
|
|
acct.CurrentBalance = 4455667788
|
|
},
|
|
originalRequest: &lnrpc.ChannelBalanceRequest{},
|
|
originalResponse: &lnrpc.ChannelBalanceResponse{
|
|
Balance: 123, // nolint
|
|
PendingOpenBalance: 456, // nolint
|
|
LocalBalance: testAmount,
|
|
RemoteBalance: testAmount,
|
|
UnsettledLocalBalance: testAmount,
|
|
UnsettledRemoteBalance: testAmount,
|
|
PendingOpenLocalBalance: testAmount,
|
|
PendingOpenRemoteBalance: testAmount,
|
|
},
|
|
replacedResponse: &lnrpc.ChannelBalanceResponse{
|
|
Balance: 4455667, // nolint
|
|
PendingOpenBalance: 0, // nolint
|
|
LocalBalance: &lnrpc.Amount{
|
|
Sat: 4455667,
|
|
Msat: 4455667788,
|
|
},
|
|
RemoteBalance: emptyAmount,
|
|
UnsettledLocalBalance: emptyAmount,
|
|
UnsettledRemoteBalance: emptyAmount,
|
|
PendingOpenLocalBalance: emptyAmount,
|
|
PendingOpenRemoteBalance: emptyAmount,
|
|
},
|
|
}, {
|
|
name: "get info",
|
|
fullURI: "/lnrpc.Lightning/GetInfo",
|
|
originalRequest: &lnrpc.GetInfoRequest{},
|
|
originalResponse: &lnrpc.GetInfoResponse{
|
|
Version: "foobar",
|
|
NumActiveChannels: 123,
|
|
NumPeers: 790,
|
|
NumPendingChannels: 777,
|
|
},
|
|
replacedResponse: &lnrpc.GetInfoResponse{
|
|
Version: "foobar",
|
|
},
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(tt *testing.T) {
|
|
tt.Parallel()
|
|
|
|
service := newMockService()
|
|
checkers := NewAccountChecker(service, chainParams)
|
|
acct := &OffChainBalanceAccount{
|
|
ID: testID,
|
|
Type: TypeInitialBalance,
|
|
Invoices: make(AccountInvoices),
|
|
Payments: make(AccountPayments),
|
|
}
|
|
ctx := AddAccountToContext(context.Background(), acct)
|
|
ctx = AddRequestIDToContext(ctx, reqID)
|
|
|
|
// Is a setup call required to initialize initial
|
|
// conditions?
|
|
if tc.setup != nil {
|
|
tc.setup(service, acct)
|
|
}
|
|
|
|
err := checkers.checkIncomingRequest(
|
|
ctx, tc.fullURI, tc.originalRequest,
|
|
)
|
|
|
|
// Did we expect an error?
|
|
if tc.requestErr != "" {
|
|
require.ErrorContains(tt, err, tc.requestErr)
|
|
return
|
|
}
|
|
require.NoError(tt, err)
|
|
|
|
replaced, err := checkers.replaceOutgoingResponse(
|
|
ctx, tc.fullURI, tc.originalResponse,
|
|
)
|
|
|
|
// Did we expect an error?
|
|
if tc.responseErr != "" {
|
|
require.ErrorContains(tt, err, tc.responseErr)
|
|
return
|
|
}
|
|
require.NoError(tt, err)
|
|
|
|
assertMessagesEqual(tt, tc.replacedResponse, replaced)
|
|
|
|
// Any post execution validation that we need to run?
|
|
if tc.validate != nil {
|
|
tc.validate(tt, service, acct)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSendPaymentCalls performs test coverage on the SendPayment and
|
|
// SendPaymentSync checkers.
|
|
func TestSendPaymentCalls(t *testing.T) {
|
|
t.Run("SendPayment", func(t *testing.T) {
|
|
testSendPayment(t, "/lnrpc.Lightning/SendPayment")
|
|
})
|
|
|
|
t.Run("SendPaymentSync", func(t *testing.T) {
|
|
testSendPayment(t, "/lnrpc.Lightning/SendPaymentSync")
|
|
})
|
|
}
|
|
|
|
func testSendPayment(t *testing.T, uri string) {
|
|
var (
|
|
ctx = context.Background()
|
|
zeroFee = &lnrpc.FeeLimit{Limit: &lnrpc.FeeLimit_Fixed{
|
|
Fixed: 0,
|
|
}}
|
|
requestID uint64
|
|
)
|
|
|
|
nextRequestID := func() uint64 {
|
|
requestID++
|
|
|
|
return requestID
|
|
}
|
|
|
|
lndMock := newMockLnd()
|
|
routerMock := newMockRouter()
|
|
errFunc := func(err error) {
|
|
lndMock.mainErrChan <- err
|
|
}
|
|
clock := clock.NewTestClock(time.Now())
|
|
store := NewTestDB(t, clock)
|
|
service, err := NewService(store, errFunc)
|
|
require.NoError(t, err)
|
|
|
|
err = service.Start(ctx, lndMock, routerMock, chainParams)
|
|
require.NoError(t, err)
|
|
|
|
assertBalance := func(id AccountID, expectedBalance int64) {
|
|
acct, err := service.Account(ctx, id)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, expectedBalance,
|
|
calcAvailableAccountBalance(acct))
|
|
}
|
|
|
|
// This should error because there is no account in the context.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &lnrpc.SendRequest{},
|
|
)
|
|
require.ErrorContains(t, err, "no account found in context")
|
|
|
|
// Create an account and add it to the context.
|
|
acct, err := service.NewAccount(
|
|
ctx, 5000, clock.Now().Add(time.Hour), "test",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
ctxWithAcct := AddAccountToContext(ctx, acct)
|
|
|
|
// This should error because there is no request ID in the context.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctxWithAcct, uri, &lnrpc.SendRequest{},
|
|
)
|
|
require.ErrorContains(t, err, "no request ID found in context")
|
|
|
|
reqID1 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID1)
|
|
|
|
// This should error because no payment hash is provided.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &lnrpc.SendRequest{},
|
|
)
|
|
require.ErrorContains(t, err, "a payment hash is required")
|
|
|
|
// This should error because of an insufficient account balance.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &lnrpc.SendRequest{
|
|
Amt: 1000,
|
|
PaymentHash: testHash[:],
|
|
},
|
|
)
|
|
require.ErrorContains(t, err, "account balance insufficient")
|
|
|
|
// Assert that the balance of the account is still un-changed since none
|
|
// of the requests have gone through yet.
|
|
assertBalance(acct.ID, 5000)
|
|
|
|
// This should work.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &lnrpc.SendRequest{
|
|
AmtMsat: 1000,
|
|
PaymentHash: testHash[:],
|
|
FeeLimit: zeroFee,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Alright, now assert that the pending amount has been accounted for.
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
// Try let the same request go through with the same payment hash. This
|
|
// should fail and the balance should remain unchanged.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &lnrpc.SendRequest{
|
|
AmtMsat: 1000,
|
|
PaymentHash: testHash[:],
|
|
FeeLimit: zeroFee,
|
|
},
|
|
)
|
|
require.ErrorContains(t, err, "is already in flight")
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
// Now let the response come through for the first request.
|
|
_, err = service.checkers.replaceOutgoingResponse(
|
|
ctx, uri, &lnrpc.SendResponse{
|
|
PaymentHash: testHash[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
// A repeated response should have no impact.
|
|
_, err = service.checkers.replaceOutgoingResponse(
|
|
ctx, uri, &lnrpc.SendResponse{
|
|
PaymentHash: testHash[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
routerMock.assertPaymentRequests(t, map[lntypes.Hash]struct{}{
|
|
testHash: {},
|
|
})
|
|
|
|
nextRequestID()
|
|
|
|
reqID2 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID2)
|
|
|
|
// Ok now we will test an errored request. First send through a valid
|
|
// send request and assert that the available balance is reduced.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &lnrpc.SendRequest{
|
|
AmtMsat: 1000,
|
|
PaymentHash: testHash2[:],
|
|
FeeLimit: zeroFee,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 3000)
|
|
|
|
// Now return an error response.
|
|
_, err = service.checkers.handleErrorResponse(
|
|
ctx, uri, nil,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// The balance should have gone back to what it was before the payment
|
|
// was initiated.
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
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.
|
|
reqID3 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID3)
|
|
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &lnrpc.SendRequest{
|
|
AmtMsat: 2000,
|
|
PaymentHash: testHash3[:],
|
|
FeeLimit: zeroFee,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 2000)
|
|
|
|
reqID4 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID4)
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &lnrpc.SendRequest{
|
|
AmtMsat: 2000,
|
|
PaymentHash: testHash4[:],
|
|
FeeLimit: zeroFee,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 0)
|
|
|
|
// Ok, now let the response for the second request come through.
|
|
_, err = service.checkers.replaceOutgoingResponse(
|
|
ctx, uri, &lnrpc.SendResponse{
|
|
PaymentHash: testHash4[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 0)
|
|
|
|
// Let the first request error.
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID3)
|
|
_, err = service.checkers.handleErrorResponse(
|
|
ctx, uri, nil,
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 2000)
|
|
}
|
|
|
|
// TestSendPaymentV2 performs test coverage on the SendPaymentV2 checker.
|
|
func TestSendPaymentV2(t *testing.T) {
|
|
var (
|
|
uri = "/routerrpc.Router/SendPaymentV2"
|
|
ctx = context.Background()
|
|
requestID uint64
|
|
)
|
|
|
|
nextRequestID := func() uint64 {
|
|
requestID++
|
|
|
|
return requestID
|
|
}
|
|
|
|
lndMock := newMockLnd()
|
|
routerMock := newMockRouter()
|
|
errFunc := func(err error) {
|
|
lndMock.mainErrChan <- err
|
|
}
|
|
clock := clock.NewTestClock(time.Now())
|
|
store := NewTestDB(t, clock)
|
|
service, err := NewService(store, errFunc)
|
|
require.NoError(t, err)
|
|
|
|
err = service.Start(ctx, lndMock, routerMock, chainParams)
|
|
require.NoError(t, err)
|
|
|
|
assertBalance := func(id AccountID, expectedBalance int64) {
|
|
acct, err := service.Account(ctx, id)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, expectedBalance,
|
|
calcAvailableAccountBalance(acct))
|
|
}
|
|
|
|
// This should error because there is no account in the context.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendPaymentRequest{},
|
|
)
|
|
require.ErrorContains(t, err, "no account found in context")
|
|
|
|
// Create an account and add it to the context.
|
|
acct, err := service.NewAccount(
|
|
ctx, 5000, clock.Now().Add(time.Hour), "test",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
ctxWithAcct := AddAccountToContext(ctx, acct)
|
|
|
|
// This should error because there is no request ID in the context.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctxWithAcct, uri, &routerrpc.SendPaymentRequest{},
|
|
)
|
|
require.ErrorContains(t, err, "no request ID found in context")
|
|
|
|
reqID1 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID1)
|
|
|
|
// This should error because no payment hash is provided.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendPaymentRequest{},
|
|
)
|
|
require.ErrorContains(t, err, "a payment hash is required")
|
|
|
|
// This should error because of an insufficient account balance.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendPaymentRequest{
|
|
Amt: 1000,
|
|
PaymentHash: testHash[:],
|
|
},
|
|
)
|
|
require.ErrorContains(t, err, "account balance insufficient")
|
|
|
|
// Assert that the balance of the account is still un-changed since none
|
|
// of the requests have gone through yet.
|
|
assertBalance(acct.ID, 5000)
|
|
|
|
// This should work.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendPaymentRequest{
|
|
AmtMsat: 1000,
|
|
PaymentHash: testHash[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Alright, now assert that the pending amount has been accounted for.
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
// Try let the same request go through with the same payment hash. This
|
|
// should fail and the balance should remain unchanged.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendPaymentRequest{
|
|
AmtMsat: 1000,
|
|
PaymentHash: testHash[:],
|
|
},
|
|
)
|
|
require.ErrorContains(t, err, "is already in flight")
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
// Now let the response come through for the first request.
|
|
_, err = service.checkers.replaceOutgoingResponse(
|
|
ctx, uri, &lnrpc.Payment{
|
|
PaymentHash: hex.EncodeToString(testHash[:]),
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
// A repeated response should have no impact.
|
|
_, err = service.checkers.replaceOutgoingResponse(
|
|
ctx, uri, &lnrpc.Payment{
|
|
PaymentHash: hex.EncodeToString(testHash[:]),
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
routerMock.assertPaymentRequests(t, map[lntypes.Hash]struct{}{
|
|
testHash: {},
|
|
})
|
|
|
|
nextRequestID()
|
|
|
|
reqID2 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID2)
|
|
|
|
// Ok now we will test an errored request. First send through a valid
|
|
// send request and assert that the available balance is reduced.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendPaymentRequest{
|
|
AmtMsat: 1000,
|
|
PaymentHash: testHash2[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 3000)
|
|
|
|
// Now return an error response.
|
|
_, err = service.checkers.handleErrorResponse(ctx, uri, nil)
|
|
require.NoError(t, err)
|
|
|
|
// The balance should have gone back to what it was before the payment
|
|
// was initiated.
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
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.
|
|
reqID3 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID3)
|
|
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendPaymentRequest{
|
|
AmtMsat: 2000,
|
|
PaymentHash: testHash3[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 2000)
|
|
|
|
reqID4 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID4)
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendPaymentRequest{
|
|
AmtMsat: 2000,
|
|
PaymentHash: testHash4[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 0)
|
|
|
|
// Ok, now let the response for the second request come through.
|
|
_, err = service.checkers.replaceOutgoingResponse(
|
|
ctx, uri, &lnrpc.Payment{
|
|
PaymentHash: hex.EncodeToString(testHash4[:]),
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 0)
|
|
|
|
// Let the first request error.
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID3)
|
|
_, err = service.checkers.handleErrorResponse(ctx, uri, nil)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 2000)
|
|
}
|
|
|
|
// TestSendToRouteV2 performs test coverage on the SendToRouteV2 checker.
|
|
func TestSendToRouteV2(t *testing.T) {
|
|
var (
|
|
uri = "/routerrpc.Router/SendToRouteV2"
|
|
ctx = context.Background()
|
|
requestID uint64
|
|
)
|
|
|
|
nextRequestID := func() uint64 {
|
|
requestID++
|
|
|
|
return requestID
|
|
}
|
|
|
|
lndMock := newMockLnd()
|
|
routerMock := newMockRouter()
|
|
errFunc := func(err error) {
|
|
lndMock.mainErrChan <- err
|
|
}
|
|
clock := clock.NewTestClock(time.Now())
|
|
store := NewTestDB(t, clock)
|
|
service, err := NewService(store, errFunc)
|
|
require.NoError(t, err)
|
|
|
|
err = service.Start(ctx, lndMock, routerMock, chainParams)
|
|
require.NoError(t, err)
|
|
|
|
assertBalance := func(id AccountID, expectedBalance int64) {
|
|
acct, err := service.Account(ctx, id)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, expectedBalance,
|
|
calcAvailableAccountBalance(acct))
|
|
}
|
|
|
|
// This should error because there is no account in the context.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendToRouteRequest{},
|
|
)
|
|
require.ErrorContains(t, err, "no account found in context")
|
|
|
|
// Create an account and add it to the context.
|
|
acct, err := service.NewAccount(
|
|
ctx, 5000, clock.Now().Add(time.Hour), "test",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
ctxWithAcct := AddAccountToContext(ctx, acct)
|
|
|
|
// This should error because there is no request ID in the context.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctxWithAcct, uri, &routerrpc.SendToRouteRequest{},
|
|
)
|
|
require.ErrorContains(t, err, "no request ID found in context")
|
|
|
|
reqID1 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID1)
|
|
|
|
// This should error because no payment hash is provided.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendToRouteRequest{},
|
|
)
|
|
require.ErrorContains(t, err, "invalid hash length")
|
|
|
|
// This should error because of an insufficient account balance.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendToRouteRequest{
|
|
Route: &lnrpc.Route{
|
|
TotalAmt: 1000,
|
|
},
|
|
PaymentHash: testHash[:],
|
|
},
|
|
)
|
|
require.ErrorContains(t, err, "account balance insufficient")
|
|
|
|
// Assert that the balance of the account is still un-changed since none
|
|
// of the requests have gone through yet.
|
|
assertBalance(acct.ID, 5000)
|
|
|
|
// This should work.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendToRouteRequest{
|
|
Route: &lnrpc.Route{
|
|
TotalAmtMsat: 1000,
|
|
},
|
|
PaymentHash: testHash[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Alright, now assert that the pending amount has been accounted for.
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
// Try let the same request go through with the same payment hash. This
|
|
// should fail and the balance should remain unchanged.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendToRouteRequest{
|
|
Route: &lnrpc.Route{
|
|
TotalAmtMsat: 1000,
|
|
},
|
|
PaymentHash: testHash[:],
|
|
},
|
|
)
|
|
require.ErrorContains(t, err, "is already in flight")
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
// Now let the response come through for the first request. Even though
|
|
// this response does not contain the payment hash, it should still be
|
|
// linked correctly since we track this in the request values store.
|
|
_, err = service.checkers.replaceOutgoingResponse(
|
|
ctx, uri, &lnrpc.HTLCAttempt{},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
// A repeated response should have no impact.
|
|
_, err = service.checkers.replaceOutgoingResponse(
|
|
ctx, uri, &lnrpc.HTLCAttempt{},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
routerMock.assertPaymentRequests(t, map[lntypes.Hash]struct{}{
|
|
testHash: {},
|
|
})
|
|
|
|
nextRequestID()
|
|
|
|
reqID2 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID2)
|
|
|
|
// Ok now we will test an errored request. First send through a valid
|
|
// send request and assert that the available balance is reduced.
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendToRouteRequest{
|
|
Route: &lnrpc.Route{
|
|
TotalAmtMsat: 1000,
|
|
},
|
|
PaymentHash: testHash2[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 3000)
|
|
|
|
// Now return an error response.
|
|
_, err = service.checkers.handleErrorResponse(ctx, uri, nil)
|
|
require.NoError(t, err)
|
|
|
|
// The balance should have gone back to what it was before the payment
|
|
// was initiated.
|
|
assertBalance(acct.ID, 4000)
|
|
|
|
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.
|
|
reqID3 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID3)
|
|
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendToRouteRequest{
|
|
Route: &lnrpc.Route{
|
|
TotalAmtMsat: 2000,
|
|
},
|
|
PaymentHash: testHash3[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 2000)
|
|
|
|
reqID4 := nextRequestID()
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID4)
|
|
err = service.checkers.checkIncomingRequest(
|
|
ctx, uri, &routerrpc.SendToRouteRequest{
|
|
Route: &lnrpc.Route{
|
|
TotalAmtMsat: 2000,
|
|
},
|
|
PaymentHash: testHash4[:],
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 0)
|
|
|
|
// Ok, now let the response for the second request come through.
|
|
_, err = service.checkers.replaceOutgoingResponse(
|
|
ctx, uri, &lnrpc.HTLCAttempt{},
|
|
)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 0)
|
|
|
|
// Let the first request error.
|
|
ctx = AddRequestIDToContext(ctxWithAcct, reqID3)
|
|
_, err = service.checkers.handleErrorResponse(ctx, uri, nil)
|
|
require.NoError(t, err)
|
|
assertBalance(acct.ID, 2000)
|
|
}
|
|
|
|
// assertMessagesEqual makes sure two proto messages are equal by JSON
|
|
// serializing them.
|
|
func assertMessagesEqual(t *testing.T, expected, actual proto.Message) {
|
|
expectedJSON, err := marshalOptions.Marshal(expected)
|
|
require.NoError(t, err)
|
|
|
|
actualJSON, err := marshalOptions.Marshal(actual)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, string(expectedJSON), string(actualJSON))
|
|
}
|