Skip to content

Commit 78d4991

Browse files
committed
itest: add integration tests for AccountPayments
Add integration tests inside itest/litd_accounts_test.go to verify retrieval of account payments against a running LND node, validating correct responses for ID/label lookup, offsets, pagination limits, and counting of total payments.
1 parent 0cbe193 commit 78d4991

1 file changed

Lines changed: 289 additions & 1 deletion

File tree

itest/litd_accounts_test.go

Lines changed: 289 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package itest
22

33
import (
44
"context"
5+
"crypto/rand"
56
"fmt"
67
"os"
8+
"sort"
79
"strings"
810
"testing"
911
"time"
@@ -14,8 +16,10 @@ import (
1416
"github.com/lightninglabs/taproot-assets/rpcutils"
1517
"github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
1618
"github.com/lightningnetwork/lnd/lnrpc"
19+
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
1720
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1821
"github.com/lightningnetwork/lnd/lntest"
22+
"github.com/lightningnetwork/lnd/lntypes"
1923
"github.com/stretchr/testify/require"
2024
"google.golang.org/grpc"
2125
)
@@ -164,12 +168,296 @@ func runAccountSystemTest(t *harnessTest, node *HarnessNode, hostPort,
164168
ctxa, t, rawConn, charlie, acctBalance,
165169
)
166170

171+
// Initiate a HODL payment (which remains IN_FLIGHT) and a FAILED
172+
// payment to verify that the AccountPayments RPC correctly retrieves
173+
// and reports payments across all potential lifecycles (SUCCEEDED,
174+
// IN_FLIGHT, FAILED).
175+
testCtx, testCancel := context.WithTimeout(ctxb, defaultTimeout)
176+
defer testCancel()
177+
178+
routerClient := routerrpc.NewRouterClient(rawConn)
179+
180+
// 1. Initiate HODL payment (IN_FLIGHT)
181+
var preimage lntypes.Preimage
182+
_, err = rand.Read(preimage[:])
183+
require.NoError(t.t, err)
184+
holdHash := preimage.Hash()
185+
186+
holdInv, err := charlie.AddHoldInvoice(
187+
testCtx, &invoicesrpc.AddHoldInvoiceRequest{
188+
Hash: holdHash[:],
189+
Value: 2222,
190+
},
191+
)
192+
require.NoError(t.t, err)
193+
194+
sendReqHold := &routerrpc.SendPaymentRequest{
195+
PaymentRequest: holdInv.PaymentRequest,
196+
TimeoutSeconds: 2,
197+
FeeLimitMsat: 1000,
198+
}
199+
holdStream, err := routerClient.SendPaymentV2(ctxa, sendReqHold)
200+
require.NoError(t.t, err)
201+
202+
holdPayment, err := getPaymentResult(holdStream, true)
203+
require.NoError(t.t, err)
204+
require.Equal(t.t, lnrpc.Payment_IN_FLIGHT, holdPayment.Status)
205+
206+
// Wait for the hold invoice to be accepted by Charlie.
207+
require.Eventually(t.t, func() bool {
208+
inv, err := charlie.LookupInvoice(
209+
testCtx, &lnrpc.PaymentHash{
210+
RHash: holdHash[:],
211+
},
212+
)
213+
214+
return err == nil &&
215+
inv.State == lnrpc.Invoice_ACCEPTED
216+
}, defaultTimeout, 100*time.Millisecond)
217+
218+
// 2. Initiate a FAILED payment
219+
var failedHash lntypes.Hash
220+
_, err = rand.Read(failedHash[:])
221+
require.NoError(t.t, err)
222+
223+
var fakePubKey [33]byte
224+
fakePubKey[0] = 0x02
225+
sendReqFailed := &routerrpc.SendPaymentRequest{
226+
Dest: fakePubKey[:],
227+
Amt: 1111,
228+
PaymentHash: failedHash[:],
229+
TimeoutSeconds: 2,
230+
FeeLimitMsat: 1000,
231+
}
232+
failedStream, err := routerClient.SendPaymentV2(ctxa, sendReqFailed)
233+
require.NoError(t.t, err)
234+
235+
failedPayment, err := getPaymentResult(failedStream, false)
236+
require.NoError(t.t, err)
237+
require.Equal(t.t, lnrpc.Payment_FAILED, failedPayment.Status)
238+
239+
// Test AccountPayments RPC with all 3 payments (succeeded,
240+
// in-flight, failed).
241+
paymentsResp, err := acctClient.AccountPayments(
242+
ctxm, &litrpc.AccountPaymentsRequest{
243+
Account: &litrpc.AccountIdentifier{
244+
Identifier: &litrpc.AccountIdentifier_Id{
245+
Id: acctResp.Account.Id,
246+
},
247+
},
248+
CountTotalPayments: true,
249+
},
250+
)
251+
require.NoError(t.t, err)
252+
require.Len(t.t, paymentsResp.Payments, 3)
253+
254+
// Sort the payments by value descending to ensure index-based
255+
// assertions are deterministic regardless of database sorting by hash.
256+
sort.Slice(paymentsResp.Payments, func(i, j int) bool {
257+
valI := paymentsResp.Payments[i].ValueSat
258+
valJ := paymentsResp.Payments[j].ValueSat
259+
260+
return valI > valJ
261+
})
262+
263+
// Check the succeeded payment. This corresponds to the 4444 sat
264+
// payment initiated earlier in testAccountRestrictions using this
265+
// account.
266+
require.Equal(
267+
t.t, uint64(4444),
268+
uint64(paymentsResp.Payments[0].ValueSat),
269+
)
270+
require.Equal(
271+
t.t, lnrpc.Payment_SUCCEEDED,
272+
paymentsResp.Payments[0].Status,
273+
)
274+
275+
// Check In-flight payment
276+
require.Equal(
277+
t.t, uint64(2222),
278+
uint64(paymentsResp.Payments[1].ValueSat),
279+
)
280+
require.Equal(
281+
t.t, lnrpc.Payment_IN_FLIGHT,
282+
paymentsResp.Payments[1].Status,
283+
)
284+
285+
// Check Failed payment
286+
require.Equal(
287+
t.t, uint64(1111),
288+
uint64(paymentsResp.Payments[2].ValueSat),
289+
)
290+
require.Equal(
291+
t.t, lnrpc.Payment_FAILED,
292+
paymentsResp.Payments[2].Status,
293+
)
294+
295+
require.EqualValues(t.t, 3, paymentsResp.TotalNumPayments)
296+
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
297+
require.EqualValues(t.t, 3, paymentsResp.LastIndexOffset)
298+
299+
// Query by label.
300+
paymentsResp, err = acctClient.AccountPayments(
301+
ctxm, &litrpc.AccountPaymentsRequest{
302+
Account: &litrpc.AccountIdentifier{
303+
Identifier: &litrpc.AccountIdentifier_Label{
304+
Label: acctLabel,
305+
},
306+
},
307+
},
308+
)
309+
require.NoError(t.t, err)
310+
require.Len(t.t, paymentsResp.Payments, 3)
311+
312+
// Query with pagination limit.
313+
paymentsResp, err = acctClient.AccountPayments(
314+
ctxm, &litrpc.AccountPaymentsRequest{
315+
Account: &litrpc.AccountIdentifier{
316+
Identifier: &litrpc.AccountIdentifier_Id{
317+
Id: acctResp.Account.Id,
318+
},
319+
},
320+
MaxPayments: 2,
321+
},
322+
)
323+
require.NoError(t.t, err)
324+
require.Len(t.t, paymentsResp.Payments, 2)
325+
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
326+
require.EqualValues(t.t, 2, paymentsResp.LastIndexOffset)
327+
328+
// Query with pagination offset out of bounds.
329+
paymentsResp, err = acctClient.AccountPayments(
330+
ctxm, &litrpc.AccountPaymentsRequest{
331+
Account: &litrpc.AccountIdentifier{
332+
Identifier: &litrpc.AccountIdentifier_Id{
333+
Id: acctResp.Account.Id,
334+
},
335+
},
336+
IndexOffset: 3,
337+
},
338+
)
339+
require.NoError(t.t, err)
340+
require.Empty(t.t, paymentsResp.Payments)
341+
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
342+
require.EqualValues(t.t, 0, paymentsResp.LastIndexOffset)
343+
344+
// Query with pagination offset inside bounds, where the number of
345+
// payments returned is fewer than MaxPayments.
346+
paymentsResp, err = acctClient.AccountPayments(
347+
ctxm, &litrpc.AccountPaymentsRequest{
348+
Account: &litrpc.AccountIdentifier{
349+
Identifier: &litrpc.AccountIdentifier_Id{
350+
Id: acctResp.Account.Id,
351+
},
352+
},
353+
IndexOffset: 2,
354+
MaxPayments: 2,
355+
},
356+
)
357+
require.NoError(t.t, err)
358+
require.Len(t.t, paymentsResp.Payments, 1)
359+
require.EqualValues(t.t, 2, paymentsResp.FirstIndexOffset)
360+
require.EqualValues(t.t, 3, paymentsResp.LastIndexOffset)
361+
362+
// Query with invalid pagination max_payments (exceeding 50).
363+
_, err = acctClient.AccountPayments(
364+
ctxm, &litrpc.AccountPaymentsRequest{
365+
Account: &litrpc.AccountIdentifier{
366+
Identifier: &litrpc.AccountIdentifier_Id{
367+
Id: acctResp.Account.Id,
368+
},
369+
},
370+
MaxPayments: 51,
371+
},
372+
)
373+
require.Error(t.t, err)
374+
require.Contains(t.t, err.Error(), "max_payments cannot exceed 50")
375+
376+
// Query with invalid pagination index_offset (exceeding 31-bit int).
377+
_, err = acctClient.AccountPayments(
378+
ctxm, &litrpc.AccountPaymentsRequest{
379+
Account: &litrpc.AccountIdentifier{
380+
Identifier: &litrpc.AccountIdentifier_Id{
381+
Id: acctResp.Account.Id,
382+
},
383+
},
384+
IndexOffset: 0x80000000,
385+
},
386+
)
387+
require.Error(t.t, err)
388+
require.Contains(t.t, err.Error(), "index_offset out of range")
389+
167390
// Test the same account restrictions with an LNC session that is bound
168391
// to the account.
169392
testAccountRestrictionsLNC(
170393
ctxm, t, rawConn, newAcctBalance, acctResp.Account.Id,
171394
)
172395

396+
// Settle the HODL invoice to clean up node state.
397+
_, err = charlie.SettleInvoice(testCtx, &invoicesrpc.SettleInvoiceMsg{
398+
Preimage: preimage[:],
399+
})
400+
require.NoError(t.t, err)
401+
402+
// Delete a single failed payment from LND to simulate a desync case
403+
// where this payment exists in LiT's account database but is
404+
// deleted/absent in LND.
405+
_, err = node.LightningClient.DeletePayment(
406+
testCtx, &lnrpc.DeletePaymentRequest{
407+
PaymentHash: failedHash[:],
408+
},
409+
)
410+
require.NoError(t.t, err)
411+
412+
// Now call AccountPayments. It should find the 3 payments in the local
413+
// store but fail to track the deleted failed payment in LND, gracefully
414+
// skipping it and returning the other 2 payments.
415+
paymentsResp, err = acctClient.AccountPayments(
416+
ctxm, &litrpc.AccountPaymentsRequest{
417+
Account: &litrpc.AccountIdentifier{
418+
Identifier: &litrpc.AccountIdentifier_Id{
419+
Id: acctResp.Account.Id,
420+
},
421+
},
422+
CountTotalPayments: true,
423+
},
424+
)
425+
require.NoError(t.t, err)
426+
require.Len(t.t, paymentsResp.Payments, 2)
427+
428+
// Sort the payments by value descending to ensure index-based
429+
// assertions are deterministic regardless of database sorting by hash.
430+
sort.Slice(paymentsResp.Payments, func(i, j int) bool {
431+
valI := paymentsResp.Payments[i].ValueSat
432+
valJ := paymentsResp.Payments[j].ValueSat
433+
434+
return valI > valJ
435+
})
436+
437+
// Succeeded payment
438+
require.Equal(
439+
t.t, uint64(4444),
440+
uint64(paymentsResp.Payments[0].ValueSat),
441+
)
442+
require.Equal(
443+
t.t, lnrpc.Payment_SUCCEEDED,
444+
paymentsResp.Payments[0].Status,
445+
)
446+
447+
// Settled hold payment
448+
require.Equal(
449+
t.t, uint64(2222),
450+
uint64(paymentsResp.Payments[1].ValueSat),
451+
)
452+
require.Equal(
453+
t.t, lnrpc.Payment_SUCCEEDED,
454+
paymentsResp.Payments[1].Status,
455+
)
456+
457+
require.EqualValues(t.t, 3, paymentsResp.TotalNumPayments)
458+
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
459+
require.EqualValues(t.t, 2, paymentsResp.LastIndexOffset)
460+
173461
// Clean up our channel and payments, so we can start the next test
174462
// iteration with a clean slate.
175463
closeChannelAndAssert(t, net, node, channelOp, false)
@@ -240,7 +528,7 @@ func testAccountRestrictionsLNC(ctxm context.Context, t *harnessTest,
240528
// There should be invoices and payments from the previous test over RPC
241529
// directly.
242530
assertNumInvoices(ctxt, t.t, lightningClient, 1)
243-
assertNumPayments(ctxt, t.t, lightningClient, 1)
531+
assertNumPayments(ctxt, t.t, lightningClient, 3)
244532
}
245533

246534
// testAccountRestrictions tests the different scenarios in which the account

0 commit comments

Comments
 (0)