lightning-terminal/itest/litd_accounts_test.go
cyberguru1 0977e375d6
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.
2026-07-22 14:06:43 -05:00

893 lines
27 KiB
Go

package itest
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"sort"
"strings"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/lightning-terminal/litrpc"
"github.com/lightninglabs/taproot-assets/rfqmath"
"github.com/lightninglabs/taproot-assets/rpcutils"
"github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
var (
burnAddr = "bcrt1qlthqw0zmup27nx35hcy82vkc4qjcxgmkvhnjtc"
)
// runAccountSystemTest tests the macaroon account system.
func runAccountSystemTest(t *harnessTest, node *HarnessNode, hostPort,
tlsCertPath, macPath string, runNumber int) {
net := t.lndHarness
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
// Before we start opening channels, we want to make sure we don't have
// any leftover funds in the tested node's wallet, so we can always
// exactly calculate what we are supposed to have during our test.
_, err := node.LightningClient.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
Addr: burnAddr,
SendAll: true,
MinConfs: 0,
SpendUnconfirmed: true,
})
require.NoError(t.t, err)
mineBlocks(t, net, 1, 1)
// Set up our channel partner Charlie that is being used to open
// channels to, send and receive payments to verify the responses of the
// different RPCs.
charlie, err := net.NewNode(t.t, "Charlie", nil, false, true)
require.NoError(t.t, err)
defer shutdownAndAssert(net, t, charlie)
const (
initialBalance = btcutil.SatoshiPerBitcoin
fundingAmt = 5_000_000
pushAmt = 2_000_000
chanReserve = 9050
chainFees = 8237
)
net.SendCoins(t.t, initialBalance, node)
net.EnsureConnected(t.t, node, charlie)
channelOp := openChannelAndAssert(
t, net, node, charlie, lntest.OpenChannelParams{
Amt: fundingAmt,
PushAmt: pushAmt,
},
)
// Make sure our normal calls all return the expected values.
localBalance := uint64(fundingAmt - pushAmt - chanReserve)
assertChannelBalance(
ctxt, t.t, node.LightningClient, localBalance, pushAmt,
)
walletBalance := int64(initialBalance - fundingAmt - chainFees)
assertWalletBalance(
ctxt, t.t, node.LightningClient, walletBalance, 0,
walletBalance, 0,
)
assertNumChannels(ctxt, t.t, node.LightningClient, 1, 0, 0, 0)
// Before the big lnd itest framework refactor the passive nodes Alice
// and Bob were always started. Now they are optional, and we don't
// start them, so we just have either Alice or Bob and the additional
// node Charlie as peers.
assertNumPeers(ctxt, t.t, node.LightningClient, 2)
// Prepare our gRPC connection with the super macaroon as the
// authentication mechanism.
rawConn, err := connectRPC(ctxt, hostPort, tlsCertPath)
require.NoError(t.t, err)
defer func() {
require.NoError(t.t, rawConn.Close())
}()
macBytes, err := os.ReadFile(macPath)
require.NoError(t.t, err)
ctxm := macaroonContext(ctxt, macBytes)
acctClient := litrpc.NewAccountsClient(rawConn)
// Create a new account with a balance of 50k sats.
const acctBalance uint64 = 50_000
acctLabel := fmt.Sprintf("test account %d", runNumber)
acctResp, err := acctClient.CreateAccount(
ctxm, &litrpc.CreateAccountRequest{
AccountBalance: acctBalance,
Label: acctLabel,
},
)
require.NoError(t.t, err)
require.NotNil(t.t, acctResp)
require.Greater(t.t, len(acctResp.Account.Id), 12)
require.EqualValues(t.t, acctBalance, acctResp.Account.CurrentBalance)
require.EqualValues(t.t, acctBalance, acctResp.Account.InitialBalance)
require.Equal(t.t, acctLabel, acctResp.Account.Label)
// Make sure we can also query the account by its name.
infoResp, err := acctClient.AccountInfo(
ctxm, &litrpc.AccountInfoRequest{
Label: acctLabel,
},
)
require.NoError(t.t, err)
require.Equal(t.t, acctResp.Account.Id, infoResp.Id)
require.EqualValues(t.t, acctBalance, infoResp.CurrentBalance)
require.EqualValues(t.t, acctBalance, infoResp.InitialBalance)
require.Equal(t.t, acctLabel, infoResp.Label)
// Now we got a new macaroon that has the account caveat attached to it.
ctxa := macaroonContext(ctxt, acctResp.Macaroon)
// We now create a few invoices with the "admin" connection and also pay
// a few invoices created by our helper node Charlie. Both the invoices
// and the payments should not show up in the responses of the account
// RPC calls as they don't belong to the account.
_, err = node.AddInvoice(ctxt, &lnrpc.Invoice{
Value: 1234,
Memo: "admin",
})
require.NoError(t.t, err)
_, err = node.AddInvoice(ctxt, &lnrpc.Invoice{
Value: 3456,
Memo: "admin",
})
require.NoError(t.t, err)
// We can't delete invoices, so there will be residual invoices from
// previous runs on the same node.
assertNumInvoices(
ctxt, t.t, node.LightningClient, runNumber*2+(runNumber-1),
)
payNode(ctxt, ctxt, t, node.RouterClient, charlie, 4567, "invoice 1")
payNode(ctxt, ctxt, t, node.RouterClient, charlie, 2345, "invoice 2")
assertNumPayments(ctxt, t.t, node.LightningClient, 2)
// Run the actual account restriction tests against the connection with
// the account macaroon.
newAcctBalance := testAccountRestrictions(
ctxa, t, rawConn, charlie, acctBalance,
)
// Initiate a HODL payment (which remains IN_FLIGHT) and a FAILED
// payment to verify that the AccountPayments RPC correctly retrieves
// and reports payments across all potential lifecycles (SUCCEEDED,
// IN_FLIGHT, FAILED).
testCtx, testCancel := context.WithTimeout(ctxb, defaultTimeout)
defer testCancel()
routerClient := routerrpc.NewRouterClient(rawConn)
// 1. Initiate HODL payment (IN_FLIGHT)
var preimage lntypes.Preimage
_, err = rand.Read(preimage[:])
require.NoError(t.t, err)
holdHash := preimage.Hash()
holdInv, err := charlie.AddHoldInvoice(
testCtx, &invoicesrpc.AddHoldInvoiceRequest{
Hash: holdHash[:],
Value: 2222,
},
)
require.NoError(t.t, err)
sendReqHold := &routerrpc.SendPaymentRequest{
PaymentRequest: holdInv.PaymentRequest,
TimeoutSeconds: 2,
FeeLimitMsat: 1000,
}
holdStream, err := routerClient.SendPaymentV2(ctxa, sendReqHold)
require.NoError(t.t, err)
holdPayment, err := getPaymentResult(holdStream, true)
require.NoError(t.t, err)
require.Equal(t.t, lnrpc.Payment_IN_FLIGHT, holdPayment.Status)
// Wait for the hold invoice to be accepted by Charlie.
require.Eventually(t.t, func() bool {
inv, err := charlie.LookupInvoice(
testCtx, &lnrpc.PaymentHash{
RHash: holdHash[:],
},
)
return err == nil &&
inv.State == lnrpc.Invoice_ACCEPTED
}, defaultTimeout, 100*time.Millisecond)
// 2. Initiate a FAILED payment
var failedHash lntypes.Hash
_, err = rand.Read(failedHash[:])
require.NoError(t.t, err)
var fakePubKey [33]byte
fakePubKey[0] = 0x02
sendReqFailed := &routerrpc.SendPaymentRequest{
Dest: fakePubKey[:],
Amt: 1111,
PaymentHash: failedHash[:],
TimeoutSeconds: 2,
FeeLimitMsat: 1000,
}
failedStream, err := routerClient.SendPaymentV2(ctxa, sendReqFailed)
require.NoError(t.t, err)
failedPayment, err := getPaymentResult(failedStream, false)
require.NoError(t.t, err)
require.Equal(t.t, lnrpc.Payment_FAILED, failedPayment.Status)
// Test AccountPayments RPC with all 3 payments (succeeded,
// in-flight, failed).
paymentsResp, err := acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
CountTotalPayments: true,
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 3)
// Sort the payments by value descending to ensure index-based
// assertions are deterministic regardless of database sorting by hash.
sort.Slice(paymentsResp.Payments, func(i, j int) bool {
valI := paymentsResp.Payments[i].ValueSat
valJ := paymentsResp.Payments[j].ValueSat
return valI > valJ
})
// Check the succeeded payment. This corresponds to the 4444 sat
// payment initiated earlier in testAccountRestrictions using this
// account.
require.Equal(
t.t, uint64(4444),
uint64(paymentsResp.Payments[0].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_SUCCEEDED,
paymentsResp.Payments[0].Status,
)
// Check In-flight payment
require.Equal(
t.t, uint64(2222),
uint64(paymentsResp.Payments[1].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_IN_FLIGHT,
paymentsResp.Payments[1].Status,
)
// Check Failed payment
require.Equal(
t.t, uint64(1111),
uint64(paymentsResp.Payments[2].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_FAILED,
paymentsResp.Payments[2].Status,
)
require.EqualValues(t.t, 3, paymentsResp.TotalNumPayments)
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 3, paymentsResp.LastIndexOffset)
// Query by label.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Label{
Label: acctLabel,
},
},
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 3)
// Query with pagination limit.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
MaxPayments: 2,
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 2)
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 2, paymentsResp.LastIndexOffset)
// Query with pagination offset out of bounds.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
IndexOffset: 3,
},
)
require.NoError(t.t, err)
require.Empty(t.t, paymentsResp.Payments)
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 0, paymentsResp.LastIndexOffset)
// Query with pagination offset inside bounds, where the number of
// payments returned is fewer than MaxPayments.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
IndexOffset: 2,
MaxPayments: 2,
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 1)
require.EqualValues(t.t, 2, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 3, paymentsResp.LastIndexOffset)
// Query with invalid pagination max_payments (exceeding 50).
_, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
MaxPayments: 51,
},
)
require.Error(t.t, err)
require.Contains(t.t, err.Error(), "max_payments cannot exceed 50")
// Query with invalid pagination index_offset (exceeding 31-bit int).
_, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
IndexOffset: 0x80000000,
},
)
require.Error(t.t, err)
require.Contains(t.t, err.Error(), "index_offset out of range")
// Test the same account restrictions with an LNC session that is bound
// to the account.
testAccountRestrictionsLNC(
ctxm, t, rawConn, newAcctBalance, acctResp.Account.Id,
)
// Make sure a payment that the account checker rejects surfaces the
// real error instead of the masked "no request values found" error.
testAccountPaymentErrorPassthrough(
ctxa, t, rawConn, charlie, newAcctBalance,
)
// Settle the HODL invoice to clean up node state.
_, err = charlie.SettleInvoice(testCtx, &invoicesrpc.SettleInvoiceMsg{
Preimage: preimage[:],
})
require.NoError(t.t, err)
// Delete a single failed payment from LND to simulate a desync case
// where this payment exists in LiT's account database but is
// deleted/absent in LND.
_, err = node.LightningClient.DeletePayment(
testCtx, &lnrpc.DeletePaymentRequest{
PaymentHash: failedHash[:],
},
)
require.NoError(t.t, err)
// Now call AccountPayments. It should find all 3 payments in the local
// store, returning a placeholder entry with status Payment_UNKNOWN for
// the deleted payment that was not found in LND.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
CountTotalPayments: true,
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 3)
// Sort the payments by value descending to ensure index-based
// assertions are deterministic regardless of database sorting by hash.
sort.Slice(paymentsResp.Payments, func(i, j int) bool {
valI := paymentsResp.Payments[i].ValueSat
valJ := paymentsResp.Payments[j].ValueSat
return valI > valJ
})
// Succeeded payment
require.Equal(
t.t, uint64(4444),
uint64(paymentsResp.Payments[0].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_SUCCEEDED,
paymentsResp.Payments[0].Status,
)
// Settled hold payment
require.Equal(
t.t, uint64(2222),
uint64(paymentsResp.Payments[1].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_SUCCEEDED,
paymentsResp.Payments[1].Status,
)
// Placeholder payment for the desynced/deleted payment
require.Equal(
t.t, hex.EncodeToString(failedHash[:]),
paymentsResp.Payments[2].PaymentHash,
)
require.Equal(
t.t, lnrpc.Payment_UNKNOWN,
paymentsResp.Payments[2].Status,
)
require.Equal(
t.t, lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
paymentsResp.Payments[2].FailureReason,
)
require.EqualValues(t.t, 3, paymentsResp.TotalNumPayments)
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 3, paymentsResp.LastIndexOffset)
// Clean up our channel and payments, so we can start the next test
// iteration with a clean slate.
closeChannelAndAssert(t, net, node, channelOp, false)
_, err = node.DeleteAllPayments(ctxt, &lnrpc.DeleteAllPaymentsRequest{
FailedPaymentsOnly: false,
FailedHtlcsOnly: false,
AllPayments: true,
})
require.NoError(t.t, err)
}
// testAccountRestrictionsLNC tests that an account restricted session can also
// be created through LNC.
func testAccountRestrictionsLNC(ctxm context.Context, t *harnessTest,
rawConn grpc.ClientConnInterface, currentAccountBalance uint64,
accountID string) {
// We first need to create an LNC session that we can use to connect.
// We use the UI password to create the session.
litClient := litrpc.NewSessionsClient(rawConn)
sessResp, err := litClient.AddSession(ctxm, &litrpc.AddSessionRequest{
Label: "integration-test",
SessionType: litrpc.SessionType_TYPE_MACAROON_ACCOUNT,
ExpiryTimestampSeconds: uint64(
time.Now().Add(5 * time.Minute).Unix(),
),
MailboxServerAddr: mailboxServerAddr,
AccountId: accountID,
})
require.NoError(t.t, err)
require.Equal(t.t, accountID, sessResp.Session.AccountId)
// Try the LNC connection now.
connectPhrase := strings.Split(
sessResp.Session.PairingSecretMnemonic, " ",
)
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
rawLNCConn, err := connectMailboxWithPairingPhrase(ctxt, connectPhrase)
require.NoError(t.t, err)
defer func() {
require.NoError(t.t, rawLNCConn.Close())
}()
lightningClient := lnrpc.NewLightningClient(rawLNCConn)
// The channel balance should always reflect our account balance.
assertChannelBalance(
ctxt, t.t, lightningClient, currentAccountBalance, 0,
)
// The on-chain balance should always be zero, no on-chain transactions
// should show up and nothing channel or peer related should be shown.
assertWalletBalance(ctxt, t.t, lightningClient, 0, 0, 0, 0)
assertNumChannels(ctxt, t.t, lightningClient, 0, 0, 0, 0)
assertNumPeers(ctxt, t.t, lightningClient, 0)
txnsResp, err := lightningClient.GetTransactions(
ctxt, &lnrpc.GetTransactionsRequest{},
)
require.NoError(t.t, err)
require.Len(t.t, txnsResp.Transactions, 0)
// There should be invoices and payments from the previous test over RPC
// directly.
assertNumInvoices(ctxt, t.t, lightningClient, 1)
assertNumPayments(ctxt, t.t, lightningClient, 3)
}
// testAccountRestrictions tests the different scenarios in which the account
// restricted RPC responses differ from the normal responses.
func testAccountRestrictions(ctxa context.Context, t *harnessTest,
rawConn grpc.ClientConnInterface, charlie *HarnessNode,
initialAccountBalance uint64) uint64 {
// The ctxa variable is the context with the restricted account macaroon
// applied to it. But we also need a timeout context for things we do
// with the charlie node.
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
// Let's do some basic validation calls against the lnrpc interface.
lightningClient := lnrpc.NewLightningClient(rawConn)
routerClient := routerrpc.NewRouterClient(rawConn)
// The channel balance should always reflect our account balance.
assertChannelBalance(
ctxa, t.t, lightningClient, initialAccountBalance, 0,
)
// The on-chain balance should always be zero, no on-chain transactions
// should show up and nothing channel or peer related should be shown.
assertWalletBalance(ctxa, t.t, lightningClient, 0, 0, 0, 0)
assertNumChannels(ctxa, t.t, lightningClient, 0, 0, 0, 0)
assertNumPeers(ctxa, t.t, lightningClient, 0)
txnsResp, err := lightningClient.GetTransactions(
ctxa, &lnrpc.GetTransactionsRequest{},
)
require.NoError(t.t, err)
require.Len(t.t, txnsResp.Transactions, 0)
// There should be no invoices or payments since they were made with the
// "admin" macaroon.
assertNumInvoices(ctxa, t.t, lightningClient, 0)
assertNumPayments(ctxa, t.t, lightningClient, 0)
// Let's now create an invoice with the account macaroon, so we can pay
// it to increase the account balance.
const inboundPaymentAmt = 7777
payNode(
ctxa, ctxt, t, charlie.RouterClient, lightningClient,
inboundPaymentAmt, "plz ser my account so poor",
)
assertNumInvoices(ctxa, t.t, lightningClient, 1)
assertNumPayments(ctxa, t.t, lightningClient, 0)
assertChannelBalance(
ctxa, t.t, lightningClient,
initialAccountBalance+inboundPaymentAmt, 0,
)
// Great, now let's also test that we can pay an invoice from the
// account which will deduct the amount from the account balance.
const outboundPaymentAmt = 4444
payNode(
ctxt, ctxa, t, routerClient, charlie, outboundPaymentAmt,
"yo, watch this",
)
assertNumInvoices(ctxa, t.t, lightningClient, 1)
assertNumPayments(ctxa, t.t, lightningClient, 1)
assertChannelBalance(
ctxa, t.t, lightningClient,
initialAccountBalance+inboundPaymentAmt-outboundPaymentAmt, 0,
)
return initialAccountBalance + inboundPaymentAmt - outboundPaymentAmt
}
// testAccountPaymentErrorPassthrough verifies that a payment which the account
// checker rejects because the account balance is insufficient surfaces a clear
// account-balance error to the caller, and never the masked "no request values
// found for request: <id>" error.
//
// SendPaymentV2 is a streaming RPC, so lnd surfaces the account checker's
// rejection through the stream's terminal error path. That reaches
// erroredPaymentHandler with no stored request values, which is exactly the
// case where the error must pass through unmasked instead of being replaced by
// a confusing "no request values found" error.
func testAccountPaymentErrorPassthrough(ctxa context.Context, t *harnessTest,
rawConn grpc.ClientConnInterface, charlie *HarnessNode,
accountBalance uint64) {
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
routerClient := routerrpc.NewRouterClient(rawConn)
// Create a routable invoice on Charlie whose amount exceeds the account
// balance, so the only possible reason the payment fails is the
// insufficient account balance.
excessiveAmt := int64(accountBalance) + 10_000
invoice, err := charlie.AddInvoice(ctxt, &lnrpc.Invoice{
Value: excessiveAmt,
Memo: "exceeds account balance",
})
require.NoError(t.t, err)
sendReq := &routerrpc.SendPaymentRequest{
PaymentRequest: invoice.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: 1000,
}
stream, err := routerClient.SendPaymentV2(ctxa, sendReq)
require.NoError(t.t, err)
// The payment must fail with the underlying account-balance error and
// not be masked by the confusing "no request values found" error.
_, err = getPaymentResult(stream, false)
require.Error(t.t, err)
require.NotContains(t.t, err.Error(), "no request values found")
require.Contains(t.t, err.Error(), "account balance insufficient")
// The account balance must be untouched by the rejected payment.
lightningClient := lnrpc.NewLightningClient(rawConn)
assertChannelBalance(ctxa, t.t, lightningClient, accountBalance, 0)
}
func assertChannelBalance(ctx context.Context, t *testing.T,
client lnrpc.LightningClient, localBalance, remoteBalance uint64) {
channelBalanceResp, err := client.ChannelBalance(
ctx, &lnrpc.ChannelBalanceRequest{},
)
require.NoError(t, err)
require.Equal(
t, int(localBalance),
int(channelBalanceResp.LocalBalance.Sat),
)
require.Equal(
t, int(remoteBalance),
int(channelBalanceResp.RemoteBalance.Sat),
)
}
func assertWalletBalance(ctx context.Context, t *testing.T,
client lnrpc.LightningClient, totalBalance, lockedBalance,
confirmedBalance, unconfirmedBalance int64) {
walletBalanceResp, err := client.WalletBalance(
ctx, &lnrpc.WalletBalanceRequest{},
)
require.NoError(t, err)
require.Equal(t, totalBalance, walletBalanceResp.TotalBalance)
require.Equal(t, lockedBalance, walletBalanceResp.LockedBalance)
require.Equal(t, confirmedBalance, walletBalanceResp.ConfirmedBalance)
require.Equal(
t, unconfirmedBalance, walletBalanceResp.UnconfirmedBalance,
)
}
func assertNumChannels(ctx context.Context, t *testing.T,
client lnrpc.LightningClient, numActive, numPendingOpen,
numPendingForceClosing, numWaitingClose int) {
listChannelsResp, err := client.ListChannels(
ctx, &lnrpc.ListChannelsRequest{},
)
require.NoError(t, err)
require.Len(t, listChannelsResp.Channels, numActive)
pendingChannelsResp, err := client.PendingChannels(
ctx, &lnrpc.PendingChannelsRequest{},
)
require.NoError(t, err)
require.Len(t, pendingChannelsResp.PendingOpenChannels, numPendingOpen)
require.Len(
t, pendingChannelsResp.PendingForceClosingChannels,
numPendingForceClosing,
)
require.Len(
t, pendingChannelsResp.WaitingCloseChannels, numWaitingClose,
)
}
func assertNumPeers(ctx context.Context, t *testing.T,
client lnrpc.LightningClient, numPeers int) {
listPeersResp, err := client.ListPeers(
ctx, &lnrpc.ListPeersRequest{},
)
require.NoError(t, err)
require.Len(t, listPeersResp.Peers, numPeers)
}
func assertNumInvoices(ctx context.Context, t *testing.T,
client lnrpc.LightningClient, numInvoices int) {
listInvoicesResp, err := client.ListInvoices(
ctx, &lnrpc.ListInvoiceRequest{},
)
require.NoError(t, err)
require.Len(t, listInvoicesResp.Invoices, numInvoices)
}
func assertNumPayments(ctx context.Context, t *testing.T,
client lnrpc.LightningClient, numPayments int) {
listPaymentsResp, err := client.ListPayments(
ctx, &lnrpc.ListPaymentsRequest{IncludeIncomplete: true},
)
require.NoError(t, err)
require.Len(t, listPaymentsResp.Payments, numPayments)
}
func payNode(invoiceCtx, paymentCtx context.Context, t *harnessTest,
from routerrpc.RouterClient, to lnrpc.LightningClient, amt int64,
memo string) {
invoice, err := to.AddInvoice(invoiceCtx, &lnrpc.Invoice{
Value: amt,
Memo: memo,
})
require.NoError(t.t, err)
sendReq := &routerrpc.SendPaymentRequest{
PaymentRequest: invoice.PaymentRequest,
TimeoutSeconds: 2,
FeeLimitMsat: 1000,
}
stream, err := from.SendPaymentV2(paymentCtx, sendReq)
require.NoError(t.t, err)
result, err := getPaymentResult(stream, false)
require.NoError(t.t, err)
require.Equal(t.t, result.Status, lnrpc.Payment_SUCCEEDED)
}
func getPaymentResult(stream routerrpc.Router_SendPaymentV2Client,
isHodl bool) (*lnrpc.Payment, error) {
for {
payment, err := stream.Recv()
if err != nil {
return nil, err
}
// If this is a hodl payment, then we'll return the first
// expected response. Otherwise, we'll wait until the in flight
// clears to we can observe the other payment states.
switch {
case isHodl:
return payment, nil
case payment.Status != lnrpc.Payment_IN_FLIGHT:
return payment, nil
}
}
}
// TapPayment encapsulates all the information related to the outcome of a tap
// asset payment. It contains the outcome of the LND payment and also the asset
// rate that was used to swap the assets to satoshis.
type TapPayment struct {
// lndPayment contains the lnd part of the payment result.
lndPayment *lnrpc.Payment
// assetRate contains the asset rate that was used to convert the assets
// to sats.
assetRate rfqmath.FixedPoint[rfqmath.BigInt]
}
func getAssetPaymentResult(t *testing.T,
s tapchannelrpc.TaprootAssetChannels_SendPaymentClient,
isHodl bool) (*TapPayment, error) {
// No idea why it makes a difference whether we wait before calling
// s.Recv() or not, but it does. Without the sleep, the test will fail
// with "insufficient local balance"... ¯\_(ツ)_/¯
// Probably something weird within lnd itself.
time.Sleep(time.Second)
var rateVal rfqmath.FixedPoint[rfqmath.BigInt]
for {
msg, err := s.Recv()
if err != nil {
return nil, err
}
// Ignore RFQ quote acceptance messages read from the send
// payment stream, as they are not relevant.
quote := msg.GetAcceptedSellOrder()
if quote != nil {
rpcRate := quote.BidAssetRate
rate, err := rpcutils.UnmarshalRfqFixedPoint(rpcRate)
require.NoError(t, err)
rateVal = *rate
t.Logf("Got quote for %v asset units per BTC from "+
"peer %v", rate, quote.Peer)
continue
}
// Ignore the new RFQ array message from the stream, it is also
// not relevant.
quotes := msg.GetAcceptedSellOrders()
if quotes != nil {
for _, quote := range quotes.AcceptedSellOrders {
rpcRate := quote.BidAssetRate
rate, err := rpcutils.UnmarshalRfqFixedPoint(
rpcRate,
)
require.NoError(t, err)
rateVal = *rate
t.Logf("Got quote for %v asset units per BTC "+
"from peer %v", rate, quote.Peer)
}
continue
}
payment := msg.GetPaymentResult()
if payment == nil {
err := fmt.Errorf("unexpected message: %v", msg)
return nil, err
}
result := &TapPayment{
lndPayment: payment,
assetRate: rateVal,
}
// If this is a hodl payment, then we'll return the first
// expected response. Otherwise, we'll wait until the in flight
// clears to we can observe the other payment states.
switch {
case isHodl:
return result, nil
case payment.Status != lnrpc.Payment_IN_FLIGHT:
return result, nil
}
}
}