diff --git a/wtxmgr/tx.go b/wtxmgr/tx.go index c90b851..7b6d772 100644 --- a/wtxmgr/tx.go +++ b/wtxmgr/tx.go @@ -317,6 +317,19 @@ func (s *Store) InsertTx(ns walletdb.ReadWriteBucket, rec *TxRecord, block *Bloc return s.insertMinedTx(ns, rec, block) } +// RemoveUnminedTx attempts to remove an unmined transaction from the +// transaction store. This is to be used in the scenario that a transaction +// that we attempt to rebroadcast, turns out to double spend one of our +// existing inputs. This function we remove the conflicting transaction +// identified by the tx record, and also recursively remove all transactions +// that depend on it. +func (s *Store) RemoveUnminedTx(ns walletdb.ReadWriteBucket, rec *TxRecord) error { + // As we already have a tx record, we can directly call the + // removeConflict method. This will do the job of recursively removing + // this unmined transaction, and any transactions that depend on it. + return s.removeConflict(ns, rec) +} + // insertMinedTx inserts a new transaction record for a mined transaction into // the database. It is expected that the exact transation does not already // exist in the unmined buckets, but unmined double spends (including mutations) diff --git a/wtxmgr/tx_test.go b/wtxmgr/tx_test.go index 3f6841c..93f205a 100644 --- a/wtxmgr/tx_test.go +++ b/wtxmgr/tx_test.go @@ -19,6 +19,7 @@ import ( "github.com/roasbeef/btcutil" "github.com/roasbeef/btcwallet/walletdb" _ "github.com/roasbeef/btcwallet/walletdb/bdb" + "github.com/roasbeef/btcwallet/wtxmgr" . "github.com/roasbeef/btcwallet/wtxmgr" ) @@ -1295,3 +1296,71 @@ func TestInsertUnserializedTx(t *testing.T) { t.Fatal("Serialized txs for coinbase spender do not match") } } + +// TestRemoveUnminedTx tests that if we add an umined transaction, then we're +// able to remove that unmined transaction later along with any of its +// descendants. Any balance modifications due to the unmined transaction should +// be revered. +func TestRemoveUnminedTx(t *testing.T) { + t.Parallel() + + store, db, teardown, err := testStore() + defer teardown() + if err != nil { + t.Fatal(err) + } + + dbtx, err := db.BeginReadWriteTx() + if err != nil { + t.Fatal(err) + } + defer dbtx.Commit() + ns := dbtx.ReadWriteBucket(namespaceKey) + + // We'll start off by adding an unconfirmed transaction to the + // transaction store. + tx := TstRecvTx + txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx.MsgTx(), time.Now()) + if err != nil { + t.Fatalf("unable to create unmined txns: %v", err) + } + if err := store.InsertTx(ns, txRec, nil); err != nil { + t.Fatalf("unable to insert transaction: %v", err) + } + + // With the transaction inserted, ensure that it's reflected in the set + // of unmined transactions. + unminedTxns, err := store.UnminedTxs(ns) + if err != nil { + t.Fatalf("unable to query for unmined txns: %v", err) + } + if len(unminedTxns) != 1 { + t.Fatalf("expected 1 mined tx, instead got %v", + len(unminedTxns)) + } + unminedTxHash := unminedTxns[0].TxHash() + txHash := tx.MsgTx().TxHash() + if !unminedTxHash.IsEqual(&txHash) { + t.Fatalf("mismatch tx hashes: expected %v, got %v", + tx.MsgTx().TxHash(), unminedTxHash) + } + + // Next, we'll delete the unmined transaction in order to simulate an + // encountered conflict. + if err := store.RemoveUnminedTx(ns, txRec); err != nil { + t.Fatalf("unable to remove unmined txns: %v", err) + } + + // If we query again for the set of unconfirmed transactions, then we + // should get an empty slice. + // With the transaction inserted, ensure that it's reflected in the set + // of unmined transactions. + unminedTxns, err = store.UnminedTxs(ns) + if err != nil { + t.Fatalf("unable to query for unmined txns: %v", err) + } + if len(unminedTxns) != 0 { + t.Fatalf("expected zero unmined txns, instead have %v", + len(unminedTxns)) + } +}