mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
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.
893 lines
27 KiB
Go
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
|
|
}
|
|
}
|
|
}
|