diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 9be9f82a56..d77e3e655b 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -111,6 +111,7 @@ BITCOIN_TESTS =\ if ENABLE_WALLET BITCOIN_TESTS += \ test/mnemonic_cleanse_tests.cpp \ + wallet/test/autoconvert_tests.cpp \ wallet/test/psbt_wallet_tests.cpp \ wallet/test/wallet_tests.cpp \ wallet/test/wallet_crypto_tests.cpp \ diff --git a/src/veil/ringct/anonwallet.cpp b/src/veil/ringct/anonwallet.cpp index 6b91027957..6b52c0e8f0 100644 --- a/src/veil/ringct/anonwallet.cpp +++ b/src/veil/ringct/anonwallet.cpp @@ -1290,6 +1290,28 @@ void AnonWallet::MarkInputsAsPendingSpend(const std::vector& vin) } } +void AnonWallet::UnwindPendingTransaction(const uint256& txid) +{ + LOCK(pwalletParent->cs_wallet); + auto mi = mapRecords.find(txid); + if (mi == mapRecords.end()) + return; + for (const COutPoint& prevout : mi->second.vin) { + auto mit = mapRecords.find(prevout.hash); + if (mit == mapRecords.end()) + continue; + COutputRecord* pout = mit->second.GetOutput(prevout.n); + if (!pout) + continue; + pout->MarkPendingSpend(false); + SaveRecord(prevout.hash, mit->second); + } + AnonWalletDB wdb(*walletDatabase); + wdb.EraseTxRecord(txid); + mapRecords.erase(mi); + LogPrintf("%s: removed unbroadcast transaction record %s\n", __func__, txid.ToString()); +} + void AnonWallet::AddOutputRecordMetaData(CTransactionRecord &rtx, std::vector &vecSend) { for (const auto &r : vecSend) { diff --git a/src/veil/ringct/anonwallet.h b/src/veil/ringct/anonwallet.h index baef69f664..52102f106c 100644 --- a/src/veil/ringct/anonwallet.h +++ b/src/veil/ringct/anonwallet.h @@ -219,6 +219,9 @@ class AnonWallet void AddOutputRecordMetaData(CTransactionRecord &rtx, std::vector &vecSend); bool ExpandTempRecipients(std::vector &vecSend, std::string &sError); void MarkInputsAsPendingSpend(const std::vector& rtxvin); + /** Remove the record of a transaction that was built but never accepted for + * broadcast, unmarking the pending spend state of its inputs. */ + void UnwindPendingTransaction(const uint256& txid); bool AddCTData(CTxOutBase *txout, CTempRecipient &r, std::string &sError); diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index e2ddd38a1d..44bad91fdd 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -23,6 +23,8 @@ void WalletInit::AddWalletOptions() const { gArgs.AddArg("-addresstype", strprintf("What type of addresses to use (\"legacy\", \"p2sh-segwit\", or \"bech32\", default: \"%s\")", FormatOutputType(DEFAULT_ADDRESS_TYPE)), false, OptionsCategory::WALLET); + gArgs.AddArg("-autoconvert", strprintf("Automatically convert confirmed basecoin and CT balances in all loaded wallets to RingCT (default: %u). Each conversion is a normal transaction that pays the standard fee. Requires the wallet to be fully unlocked", DEFAULT_AUTOCONVERT), false, OptionsCategory::WALLET); + gArgs.AddArg("-autoconvertthreshold=", strprintf("Minimum amount of eligible outputs required before an automatic conversion transaction is created (default: %s)", FormatMoney(DEFAULT_AUTOCONVERT_THRESHOLD)), false, OptionsCategory::WALLET); gArgs.AddArg("-avoidpartialspends", strprintf(_("Group outputs by address, selecting all or none, instead of selecting on a per-output basis. Privacy is improved as an address is only used once (unless someone sends to it after spending from it), but may result in slightly higher fees as suboptimal coin selection may result due to the added limitation (default: %u)"), DEFAULT_AVOIDPARTIALSPENDS), false, OptionsCategory::WALLET); gArgs.AddArg("-backupzerocoin", "Enable automatic backups of zerocoin", false, OptionsCategory::WALLET); gArgs.AddArg("-changetype", "What type of change to use (\"legacy\", \"p2sh-segwit\", or \"bech32\"). Default is same as -addresstype, except when -addresstype=p2sh-segwit a native segwit output is used when sending to a native segwit address)", false, OptionsCategory::WALLET); @@ -247,6 +249,14 @@ void WalletInit::Start(CScheduler& scheduler) const // Run a thread to flush wallet periodically scheduler.scheduleEvery(MaybeCompactWalletDB, 500); + + if (gArgs.GetBoolArg("-autoconvert", DEFAULT_AUTOCONVERT)) { + scheduler.scheduleEvery([]() { + for (const std::shared_ptr& pwallet : GetWallets()) { + pwallet->AutoConvertToRingCT(); + } + }, AUTOCONVERT_TIMER_INTERVAL_MS); + } } void WalletInit::Flush() const diff --git a/src/wallet/test/autoconvert_tests.cpp b/src/wallet/test/autoconvert_tests.cpp new file mode 100644 index 0000000000..0b0e4d2539 --- /dev/null +++ b/src/wallet/test/autoconvert_tests.cpp @@ -0,0 +1,305 @@ +// Copyright (c) 2026 The Veil developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +// Tests for the -autoconvert failure handling (wallet.cpp AutoConvertToRingCT): +// a conversion transaction that is rejected by the mempool must leave no +// trace in the wallet — no transaction record, no inputs stuck in the +// pending-spend state, no balance change — and the backoff must delay the +// next attempt. This is the one part of the feature that mainnet happy-path +// testing cannot exercise. + +#include + +#include +#include +#include