diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 3370e3fe6..73f3e62c5 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -558,4 +558,8 @@ var allTestCases = []*lntest.TestCase{ Name: "query blinded route", TestFunc: testQueryBlindedRoutes, }, + { + Name: "removetx", + TestFunc: testRemoveTx, + }, } diff --git a/itest/lnd_onchain_test.go b/itest/lnd_onchain_test.go index 7bd97c8bd..6068832e0 100644 --- a/itest/lnd_onchain_test.go +++ b/itest/lnd_onchain_test.go @@ -218,7 +218,7 @@ func testCPFP(ht *lntest.HarnessTest) { runCPFP(ht, ht.Alice, ht.Bob) } -// runCPFP ensures that the daemon can bump an unconfirmed transaction's fee +// runCPFP ensures that the daemon can bump an unconfirmed transaction's fee // rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction. func runCPFP(ht *lntest.HarnessTest, alice, bob *node.HarnessNode) { // Skip this test for neutrino, as it's not aware of mempool @@ -731,3 +731,104 @@ func genAnchorSweep(ht *lntest.HarnessTest, return btcutil.NewTx(tx) } + +// testRemoveTx tests that we are able to remove an unconfirmed transaction +// from the internal wallet as long as the tx is still unconfirmed. This test +// also verifies that after the tx is removed (while unconfirmed) it will show +// up as confirmed as soon as the original transaction is mined. +func testRemoveTx(ht *lntest.HarnessTest) { + // Create a new node so that we start with no funds on the internal + // wallet. + alice := ht.NewNode("Alice", nil) + + const initialWalletAmt = btcutil.SatoshiPerBitcoin + + // Funding the node with an initial balance. + ht.FundCoins(initialWalletAmt, alice) + + // Create an address for Alice to send the coins to. + req := &lnrpc.NewAddressRequest{ + Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, + } + resp := alice.RPC.NewAddress(req) + + // We send half the amount to that address generating two unconfirmed + // outpoints in our internal wallet. + sendReq := &lnrpc.SendCoinsRequest{ + Addr: resp.Address, + Amount: initialWalletAmt / 2, + } + alice.RPC.SendCoins(sendReq) + txID := ht.Miner.AssertNumTxsInMempool(1)[0] + + // Make sure the unspent number of utxos is 2 and the unconfirmed + // balances add up. + unconfirmed := ht.GetUTXOsUnconfirmed( + alice, lnwallet.DefaultAccountName, + ) + require.Lenf(ht, unconfirmed, 2, "number of unconfirmed tx") + + // Get the raw transaction to calculate the exact fee. + tx := ht.Miner.GetNumTxsFromMempool(1)[0] + + // Calculate the tx fee so we can compare the end amounts. We are + // sending from the internal wallet to the internal wallet so only + // the tx fee applies when calucalting the final amount of the wallet. + txFee := ht.CalculateTxFee(tx) + + // All of alice's balance is unconfirmed and equals the initial amount + // minus the tx fee. + aliceBalResp := alice.RPC.WalletBalance() + expectedAmt := btcutil.Amount(initialWalletAmt) - txFee + require.EqualValues(ht, expectedAmt, aliceBalResp.UnconfirmedBalance) + + // Now remove the transaction. We should see that the wallet state + // equals the amount prior to sending the transaction. It is important + // to understand that we do not remove any transaction from the mempool + // (thats not possible in reality) we just remove it from our local + // store. + var buf bytes.Buffer + require.NoError(ht, tx.Serialize(&buf)) + alice.RPC.RemoveTransaction(&walletrpc.GetTransactionRequest{ + Txid: txID.String(), + }) + + // Verify that the balance equals the initial state. + confirmed := ht.GetUTXOsConfirmed( + alice, lnwallet.DefaultAccountName, + ) + require.Lenf(ht, confirmed, 1, "number confirmed tx") + + // Alice's balance should be the initial balance now because all the + // unconfirmed tx got removed. + aliceBalResp = alice.RPC.WalletBalance() + expectedAmt = btcutil.Amount(initialWalletAmt) + require.EqualValues(ht, expectedAmt, aliceBalResp.ConfirmedBalance) + + // Mine a block and make sure the transaction previously broadcasted + // shows up in alice's wallet although we removed the transaction from + // the wallet when it was unconfirmed. + block := ht.Miner.MineBlocks(1)[0] + ht.Miner.AssertTxInBlock(block, txID) + + // Verify that alice has 2 confirmed unspent utxos in her default + // wallet. + err := wait.NoError(func() error { + confirmed = ht.GetUTXOsConfirmed( + alice, lnwallet.DefaultAccountName, + ) + if len(confirmed) != 2 { + return fmt.Errorf("expected 2 confirmed tx, "+ + " got %v", len(confirmed)) + } + + return nil + }, lntest.DefaultTimeout) + require.NoError(ht, err, "timeout checking for confirmed utxos") + + // The remaining balance should equal alice's starting balance minus the + // tx fee. + aliceBalResp = alice.RPC.WalletBalance() + expectedAmt = btcutil.Amount(initialWalletAmt) - txFee + require.EqualValues(ht, expectedAmt, aliceBalResp.ConfirmedBalance) +} diff --git a/lntest/rpc/wallet_kit.go b/lntest/rpc/wallet_kit.go index 5e330192f..2b403715f 100644 --- a/lntest/rpc/wallet_kit.go +++ b/lntest/rpc/wallet_kit.go @@ -210,6 +210,22 @@ func (h *HarnessRPC) GetTransaction( return resp } +// RemoveTransaction makes an RPC call to the node's WalletKitClient and +// asserts. +// +//nolint:lll +func (h *HarnessRPC) RemoveTransaction( + req *walletrpc.GetTransactionRequest) *walletrpc.RemoveTransactionResponse { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.WalletKit.RemoveTransaction(ctxt, req) + h.NoError(err, "RemoveTransaction") + + return resp +} + // BumpFee makes a RPC call to the node's WalletKitClient and asserts. func (h *HarnessRPC) BumpFee( req *walletrpc.BumpFeeRequest) *walletrpc.BumpFeeResponse {