mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 23:09:44 -04:00
Merge bitcoin/bitcoin#26878: [24.x] Backports
784a754aa4
wallet, rpc: Update migratewallet help text for encrypted wallets (Andrew Chow)debcfe313a
tests: Tests for migrating wallets by name, and providing passphrase (Andrew Chow)ccc72fecd7
wallet: Be able to unlock the wallet for migration (Andrew Chow)50dd8b13df
rpc: Allow users to specify wallet name for migratewallet (Andrew Chow)648b06256d
wallet: Allow MigrateLegacyToDescriptor to take a wallet name (Andrew Chow)ab3bd457cd
i2p: use consistent number of tunnels with i2pd and Java I2P (Vasil Dimov)29cdf42226
i2p: lower the number of tunnels for transient sessions (Vasil Dimov)5027e93b6a
i2p: reuse created I2P sessions if not used (Vasil Dimov)a62c541ae8
wallet: reuse change dest when recreating TX with avoidpartialspends (Matthew Zipkin)64e7db6f4f
Zero out wallet master key upon lock (John Moffett)b7e242ecb3
Correctly limit overview transaction list (John Moffett)cff67180b3
depends: fix systemtap download URL (fanquake)7cf73dfed5
Add missing includes to fix gcc-13 compile error (MarcoFalke)07397cdede
addrdb: Only call Serialize() once (Martin Zumsande)91f83dbeb1
hash: add HashedSourceWriter (Martin Zumsande)5c824ac5e1
For feebump, ignore abandoned descendant spends (John Moffett)428dcd51e6
wallet: Skip rescanning if wallet is more recent than tip (Andrew Chow)cbcdafa471
test: wallet: check that labels are migrated to watchonly wallet (Sebastian Falbesoner)342abfb3f4
wallet: fully migrate address book entries for watchonly/solvable wallets (Sebastian Falbesoner) Pull request description: Backports: * https://github.com/bitcoin/bitcoin/pull/26595 * https://github.com/bitcoin/bitcoin/pull/26675 * https://github.com/bitcoin/bitcoin/pull/26679 * https://github.com/bitcoin/bitcoin/pull/26761 * https://github.com/bitcoin/bitcoin/pull/26837 * https://github.com/bitcoin/bitcoin/pull/26909 * https://github.com/bitcoin/bitcoin/pull/26924 * https://github.com/bitcoin/bitcoin/pull/26944 * https://github.com/bitcoin-core/gui/pull/704 * https://github.com/bitcoin/bitcoin/pull/27053 * https://github.com/bitcoin/bitcoin/pull/27080 ACKs for top commit: instagibbs: ACK784a754aa4
achow101: ACK784a754aa4
hebasto: ACK784a754aa4
, I've made backporting locally and got a diff between my branch and this PR as follows: Tree-SHA512: 8ea84aa02d7907ff1e202e1302b441ce9ed2198bf383620ad40056a5d7e8ea88e1047abef0b92d85648016bf9b3195c974be3806ccebd85bef4f85c326869e43
This commit is contained in:
commit
c8c85ca16e
23 changed files with 447 additions and 95 deletions
|
@ -1,6 +1,6 @@
|
||||||
package=systemtap
|
package=systemtap
|
||||||
$(package)_version=4.7
|
$(package)_version=4.7
|
||||||
$(package)_download_path=https://sourceware.org/systemtap/ftp/releases/
|
$(package)_download_path=https://sourceware.org/ftp/systemtap/releases/
|
||||||
$(package)_file_name=$(package)-$($(package)_version).tar.gz
|
$(package)_file_name=$(package)-$($(package)_version).tar.gz
|
||||||
$(package)_sha256_hash=43a0a3db91aa4d41e28015b39a65e62059551f3cc7377ebf3a3a5ca7339e7b1f
|
$(package)_sha256_hash=43a0a3db91aa4d41e28015b39a65e62059551f3cc7377ebf3a3a5ca7339e7b1f
|
||||||
$(package)_patches=remove_SDT_ASM_SECTION_AUTOGROUP_SUPPORT_check.patch
|
$(package)_patches=remove_SDT_ASM_SECTION_AUTOGROUP_SUPPORT_check.patch
|
||||||
|
|
|
@ -34,10 +34,9 @@ bool SerializeDB(Stream& stream, const Data& data)
|
||||||
{
|
{
|
||||||
// Write and commit header, data
|
// Write and commit header, data
|
||||||
try {
|
try {
|
||||||
CHashWriter hasher(stream.GetType(), stream.GetVersion());
|
HashedSourceWriter hashwriter{stream};
|
||||||
stream << Params().MessageStart() << data;
|
hashwriter << Params().MessageStart() << data;
|
||||||
hasher << Params().MessageStart() << data;
|
stream << hashwriter.GetHash();
|
||||||
stream << hasher.GetHash();
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
return error("%s: Serialize or I/O error - %s", __func__, e.what());
|
return error("%s: Serialize or I/O error - %s", __func__, e.what());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1178,8 +1178,7 @@ void AddrMan::Unserialize(Stream& s_)
|
||||||
}
|
}
|
||||||
|
|
||||||
// explicit instantiation
|
// explicit instantiation
|
||||||
template void AddrMan::Serialize(CHashWriter& s) const;
|
template void AddrMan::Serialize(HashedSourceWriter<CAutoFile>& s) const;
|
||||||
template void AddrMan::Serialize(CAutoFile& s) const;
|
|
||||||
template void AddrMan::Serialize(CDataStream& s) const;
|
template void AddrMan::Serialize(CDataStream& s) const;
|
||||||
template void AddrMan::Unserialize(CAutoFile& s);
|
template void AddrMan::Unserialize(CAutoFile& s);
|
||||||
template void AddrMan::Unserialize(CHashVerifier<CAutoFile>& s);
|
template void AddrMan::Unserialize(CHashVerifier<CAutoFile>& s);
|
||||||
|
|
25
src/hash.h
25
src/hash.h
|
@ -6,6 +6,7 @@
|
||||||
#ifndef BITCOIN_HASH_H
|
#ifndef BITCOIN_HASH_H
|
||||||
#define BITCOIN_HASH_H
|
#define BITCOIN_HASH_H
|
||||||
|
|
||||||
|
#include <attributes.h>
|
||||||
#include <crypto/common.h>
|
#include <crypto/common.h>
|
||||||
#include <crypto/ripemd160.h>
|
#include <crypto/ripemd160.h>
|
||||||
#include <crypto/sha256.h>
|
#include <crypto/sha256.h>
|
||||||
|
@ -199,6 +200,30 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Writes data to an underlying source stream, while hashing the written data. */
|
||||||
|
template <typename Source>
|
||||||
|
class HashedSourceWriter : public CHashWriter
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
Source& m_source;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit HashedSourceWriter(Source& source LIFETIMEBOUND) : CHashWriter{source.GetType(), source.GetVersion()}, m_source{source} {}
|
||||||
|
|
||||||
|
void write(Span<const std::byte> src)
|
||||||
|
{
|
||||||
|
m_source.write(src);
|
||||||
|
CHashWriter::write(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
HashedSourceWriter& operator<<(const T& obj)
|
||||||
|
{
|
||||||
|
::Serialize(*this, obj);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** Compute the 256-bit hash of an object's serialization. */
|
/** Compute the 256-bit hash of an object's serialization. */
|
||||||
template<typename T>
|
template<typename T>
|
||||||
uint256 SerializeHash(const T& obj, int nType=SER_GETHASH, int nVersion=PROTOCOL_VERSION)
|
uint256 SerializeHash(const T& obj, int nType=SER_GETHASH, int nVersion=PROTOCOL_VERSION)
|
||||||
|
|
|
@ -379,7 +379,9 @@ void Session::CreateIfNotCreatedAlready()
|
||||||
// in the reply in DESTINATION=.
|
// in the reply in DESTINATION=.
|
||||||
const Reply& reply = SendRequestAndGetReply(
|
const Reply& reply = SendRequestAndGetReply(
|
||||||
*sock,
|
*sock,
|
||||||
strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT SIGNATURE_TYPE=7", session_id));
|
strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT SIGNATURE_TYPE=7 "
|
||||||
|
"inbound.quantity=1 outbound.quantity=1",
|
||||||
|
session_id));
|
||||||
|
|
||||||
m_private_key = DecodeI2PBase64(reply.Get("DESTINATION"));
|
m_private_key = DecodeI2PBase64(reply.Get("DESTINATION"));
|
||||||
} else {
|
} else {
|
||||||
|
@ -395,7 +397,8 @@ void Session::CreateIfNotCreatedAlready()
|
||||||
const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
|
const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
|
||||||
|
|
||||||
SendRequestAndGetReply(*sock,
|
SendRequestAndGetReply(*sock,
|
||||||
strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s",
|
strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s "
|
||||||
|
"inbound.quantity=3 outbound.quantity=3",
|
||||||
session_id,
|
session_id,
|
||||||
private_key_b64));
|
private_key_b64));
|
||||||
}
|
}
|
||||||
|
|
23
src/net.cpp
23
src/net.cpp
|
@ -436,6 +436,7 @@ static CAddress GetBindAddress(const Sock& sock)
|
||||||
|
|
||||||
CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type)
|
CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type)
|
||||||
{
|
{
|
||||||
|
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
|
||||||
assert(conn_type != ConnectionType::INBOUND);
|
assert(conn_type != ConnectionType::INBOUND);
|
||||||
|
|
||||||
if (pszDest == nullptr) {
|
if (pszDest == nullptr) {
|
||||||
|
@ -496,8 +497,23 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
|
||||||
if (m_i2p_sam_session) {
|
if (m_i2p_sam_session) {
|
||||||
connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed);
|
connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed);
|
||||||
} else {
|
} else {
|
||||||
i2p_transient_session = std::make_unique<i2p::sam::Session>(proxy.proxy, &interruptNet);
|
{
|
||||||
|
LOCK(m_unused_i2p_sessions_mutex);
|
||||||
|
if (m_unused_i2p_sessions.empty()) {
|
||||||
|
i2p_transient_session =
|
||||||
|
std::make_unique<i2p::sam::Session>(proxy.proxy, &interruptNet);
|
||||||
|
} else {
|
||||||
|
i2p_transient_session.swap(m_unused_i2p_sessions.front());
|
||||||
|
m_unused_i2p_sessions.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
connected = i2p_transient_session->Connect(addrConnect, conn, proxyConnectionFailed);
|
connected = i2p_transient_session->Connect(addrConnect, conn, proxyConnectionFailed);
|
||||||
|
if (!connected) {
|
||||||
|
LOCK(m_unused_i2p_sessions_mutex);
|
||||||
|
if (m_unused_i2p_sessions.size() < MAX_UNUSED_I2P_SESSIONS_SIZE) {
|
||||||
|
m_unused_i2p_sessions.emplace(i2p_transient_session.release());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
@ -1048,6 +1064,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr<Sock>&& sock,
|
||||||
|
|
||||||
bool CConnman::AddConnection(const std::string& address, ConnectionType conn_type)
|
bool CConnman::AddConnection(const std::string& address, ConnectionType conn_type)
|
||||||
{
|
{
|
||||||
|
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
|
||||||
std::optional<int> max_connections;
|
std::optional<int> max_connections;
|
||||||
switch (conn_type) {
|
switch (conn_type) {
|
||||||
case ConnectionType::INBOUND:
|
case ConnectionType::INBOUND:
|
||||||
|
@ -1510,6 +1527,7 @@ void CConnman::DumpAddresses()
|
||||||
|
|
||||||
void CConnman::ProcessAddrFetch()
|
void CConnman::ProcessAddrFetch()
|
||||||
{
|
{
|
||||||
|
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
|
||||||
std::string strDest;
|
std::string strDest;
|
||||||
{
|
{
|
||||||
LOCK(m_addr_fetches_mutex);
|
LOCK(m_addr_fetches_mutex);
|
||||||
|
@ -1578,6 +1596,7 @@ int CConnman::GetExtraBlockRelayCount() const
|
||||||
|
|
||||||
void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
|
void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
|
||||||
{
|
{
|
||||||
|
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
|
||||||
SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_OPEN_CONNECTION);
|
SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_OPEN_CONNECTION);
|
||||||
FastRandomContext rng;
|
FastRandomContext rng;
|
||||||
// Connect to specific addresses
|
// Connect to specific addresses
|
||||||
|
@ -1929,6 +1948,7 @@ std::vector<AddedNodeInfo> CConnman::GetAddedNodeInfo() const
|
||||||
|
|
||||||
void CConnman::ThreadOpenAddedConnections()
|
void CConnman::ThreadOpenAddedConnections()
|
||||||
{
|
{
|
||||||
|
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
|
||||||
SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_ADD_CONNECTION);
|
SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_ADD_CONNECTION);
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
@ -1958,6 +1978,7 @@ void CConnman::ThreadOpenAddedConnections()
|
||||||
// if successful, this moves the passed grant to the constructed node
|
// if successful, this moves the passed grant to the constructed node
|
||||||
void CConnman::OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant *grantOutbound, const char *pszDest, ConnectionType conn_type)
|
void CConnman::OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant *grantOutbound, const char *pszDest, ConnectionType conn_type)
|
||||||
{
|
{
|
||||||
|
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
|
||||||
assert(conn_type != ConnectionType::INBOUND);
|
assert(conn_type != ConnectionType::INBOUND);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
33
src/net.h
33
src/net.h
|
@ -38,6 +38,7 @@
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <queue>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
@ -744,7 +745,7 @@ public:
|
||||||
bool GetNetworkActive() const { return fNetworkActive; };
|
bool GetNetworkActive() const { return fNetworkActive; };
|
||||||
bool GetUseAddrmanOutgoing() const { return m_use_addrman_outgoing; };
|
bool GetUseAddrmanOutgoing() const { return m_use_addrman_outgoing; };
|
||||||
void SetNetworkActive(bool active);
|
void SetNetworkActive(bool active);
|
||||||
void OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant* grantOutbound, const char* strDest, ConnectionType conn_type);
|
void OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant* grantOutbound, const char* strDest, ConnectionType conn_type) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
|
||||||
bool CheckIncomingNonce(uint64_t nonce);
|
bool CheckIncomingNonce(uint64_t nonce);
|
||||||
|
|
||||||
bool ForNode(NodeId id, std::function<bool(CNode* pnode)> func);
|
bool ForNode(NodeId id, std::function<bool(CNode* pnode)> func);
|
||||||
|
@ -820,7 +821,7 @@ public:
|
||||||
* - Max total outbound connection capacity filled
|
* - Max total outbound connection capacity filled
|
||||||
* - Max connection capacity for type is filled
|
* - Max connection capacity for type is filled
|
||||||
*/
|
*/
|
||||||
bool AddConnection(const std::string& address, ConnectionType conn_type);
|
bool AddConnection(const std::string& address, ConnectionType conn_type) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
|
||||||
|
|
||||||
size_t GetNodeCount(ConnectionDirection) const;
|
size_t GetNodeCount(ConnectionDirection) const;
|
||||||
void GetNodeStats(std::vector<CNodeStats>& vstats) const;
|
void GetNodeStats(std::vector<CNodeStats>& vstats) const;
|
||||||
|
@ -886,10 +887,10 @@ private:
|
||||||
bool Bind(const CService& addr, unsigned int flags, NetPermissionFlags permissions);
|
bool Bind(const CService& addr, unsigned int flags, NetPermissionFlags permissions);
|
||||||
bool InitBinds(const Options& options);
|
bool InitBinds(const Options& options);
|
||||||
|
|
||||||
void ThreadOpenAddedConnections() EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex);
|
void ThreadOpenAddedConnections() EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex, !m_unused_i2p_sessions_mutex);
|
||||||
void AddAddrFetch(const std::string& strDest) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex);
|
void AddAddrFetch(const std::string& strDest) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex);
|
||||||
void ProcessAddrFetch() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex);
|
void ProcessAddrFetch() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_unused_i2p_sessions_mutex);
|
||||||
void ThreadOpenConnections(std::vector<std::string> connect) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex);
|
void ThreadOpenConnections(std::vector<std::string> connect) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !m_unused_i2p_sessions_mutex);
|
||||||
void ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc);
|
void ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc);
|
||||||
void ThreadI2PAcceptIncoming();
|
void ThreadI2PAcceptIncoming();
|
||||||
void AcceptConnection(const ListenSocket& hListenSocket);
|
void AcceptConnection(const ListenSocket& hListenSocket);
|
||||||
|
@ -956,7 +957,7 @@ private:
|
||||||
bool AlreadyConnectedToAddress(const CAddress& addr);
|
bool AlreadyConnectedToAddress(const CAddress& addr);
|
||||||
|
|
||||||
bool AttemptToEvictConnection();
|
bool AttemptToEvictConnection();
|
||||||
CNode* ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type);
|
CNode* ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
|
||||||
void AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr) const;
|
void AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr) const;
|
||||||
|
|
||||||
void DeleteNode(CNode* pnode);
|
void DeleteNode(CNode* pnode);
|
||||||
|
@ -1127,6 +1128,26 @@ private:
|
||||||
*/
|
*/
|
||||||
std::vector<CService> m_onion_binds;
|
std::vector<CService> m_onion_binds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutex protecting m_i2p_sam_sessions.
|
||||||
|
*/
|
||||||
|
Mutex m_unused_i2p_sessions_mutex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pool of created I2P SAM transient sessions that should be used instead
|
||||||
|
* of creating new ones in order to reduce the load on the I2P network.
|
||||||
|
* Creating a session in I2P is not cheap, thus if this is not empty, then
|
||||||
|
* pick an entry from it instead of creating a new session. If connecting to
|
||||||
|
* a host fails, then the created session is put to this pool for reuse.
|
||||||
|
*/
|
||||||
|
std::queue<std::unique_ptr<i2p::sam::Session>> m_unused_i2p_sessions GUARDED_BY(m_unused_i2p_sessions_mutex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cap on the size of `m_unused_i2p_sessions`, to ensure it does not
|
||||||
|
* unexpectedly use too much memory.
|
||||||
|
*/
|
||||||
|
static constexpr size_t MAX_UNUSED_I2P_SESSIONS_SIZE{10};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RAII helper to atomically create a copy of `m_nodes` and add a reference
|
* RAII helper to atomically create a copy of `m_nodes` and add a reference
|
||||||
* to each of the nodes. The nodes are released when this object is destroyed.
|
* to each of the nodes. The nodes are released when this object is destroyed.
|
||||||
|
|
|
@ -264,7 +264,6 @@ void OverviewPage::setWalletModel(WalletModel *model)
|
||||||
// Set up transaction list
|
// Set up transaction list
|
||||||
filter.reset(new TransactionFilterProxy());
|
filter.reset(new TransactionFilterProxy());
|
||||||
filter->setSourceModel(model->getTransactionTableModel());
|
filter->setSourceModel(model->getTransactionTableModel());
|
||||||
filter->setLimit(NUM_ITEMS);
|
|
||||||
filter->setDynamicSortFilter(true);
|
filter->setDynamicSortFilter(true);
|
||||||
filter->setSortRole(Qt::EditRole);
|
filter->setSortRole(Qt::EditRole);
|
||||||
filter->setShowInactive(false);
|
filter->setShowInactive(false);
|
||||||
|
@ -273,6 +272,10 @@ void OverviewPage::setWalletModel(WalletModel *model)
|
||||||
ui->listTransactions->setModel(filter.get());
|
ui->listTransactions->setModel(filter.get());
|
||||||
ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress);
|
ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress);
|
||||||
|
|
||||||
|
connect(filter.get(), &TransactionFilterProxy::rowsInserted, this, &OverviewPage::LimitTransactionRows);
|
||||||
|
connect(filter.get(), &TransactionFilterProxy::rowsRemoved, this, &OverviewPage::LimitTransactionRows);
|
||||||
|
connect(filter.get(), &TransactionFilterProxy::rowsMoved, this, &OverviewPage::LimitTransactionRows);
|
||||||
|
LimitTransactionRows();
|
||||||
// Keep up to date with wallet
|
// Keep up to date with wallet
|
||||||
setBalance(model->getCachedBalance());
|
setBalance(model->getCachedBalance());
|
||||||
connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance);
|
connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance);
|
||||||
|
@ -301,6 +304,16 @@ void OverviewPage::changeEvent(QEvent* e)
|
||||||
QWidget::changeEvent(e);
|
QWidget::changeEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only show most recent NUM_ITEMS rows
|
||||||
|
void OverviewPage::LimitTransactionRows()
|
||||||
|
{
|
||||||
|
if (filter && ui->listTransactions && ui->listTransactions->model() && filter.get() == ui->listTransactions->model()) {
|
||||||
|
for (int i = 0; i < filter->rowCount(); ++i) {
|
||||||
|
ui->listTransactions->setRowHidden(i, i >= NUM_ITEMS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void OverviewPage::updateDisplayUnit()
|
void OverviewPage::updateDisplayUnit()
|
||||||
{
|
{
|
||||||
if (walletModel && walletModel->getOptionsModel()) {
|
if (walletModel && walletModel->getOptionsModel()) {
|
||||||
|
|
|
@ -60,6 +60,7 @@ private:
|
||||||
std::unique_ptr<TransactionFilterProxy> filter;
|
std::unique_ptr<TransactionFilterProxy> filter;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
|
void LimitTransactionRows();
|
||||||
void updateDisplayUnit();
|
void updateDisplayUnit();
|
||||||
void handleTransactionClicked(const QModelIndex &index);
|
void handleTransactionClicked(const QModelIndex &index);
|
||||||
void updateAlerts(const QString &warnings);
|
void updateAlerts(const QString &warnings);
|
||||||
|
|
|
@ -17,7 +17,6 @@ TransactionFilterProxy::TransactionFilterProxy(QObject *parent) :
|
||||||
typeFilter(ALL_TYPES),
|
typeFilter(ALL_TYPES),
|
||||||
watchOnlyFilter(WatchOnlyFilter_All),
|
watchOnlyFilter(WatchOnlyFilter_All),
|
||||||
minAmount(0),
|
minAmount(0),
|
||||||
limitRows(-1),
|
|
||||||
showInactive(true)
|
showInactive(true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -92,25 +91,8 @@ void TransactionFilterProxy::setWatchOnlyFilter(WatchOnlyFilter filter)
|
||||||
invalidateFilter();
|
invalidateFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TransactionFilterProxy::setLimit(int limit)
|
|
||||||
{
|
|
||||||
this->limitRows = limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TransactionFilterProxy::setShowInactive(bool _showInactive)
|
void TransactionFilterProxy::setShowInactive(bool _showInactive)
|
||||||
{
|
{
|
||||||
this->showInactive = _showInactive;
|
this->showInactive = _showInactive;
|
||||||
invalidateFilter();
|
invalidateFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
int TransactionFilterProxy::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
if(limitRows != -1)
|
|
||||||
{
|
|
||||||
return std::min(QSortFilterProxyModel::rowCount(parent), limitRows);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return QSortFilterProxyModel::rowCount(parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -42,14 +42,9 @@ public:
|
||||||
void setMinAmount(const CAmount& minimum);
|
void setMinAmount(const CAmount& minimum);
|
||||||
void setWatchOnlyFilter(WatchOnlyFilter filter);
|
void setWatchOnlyFilter(WatchOnlyFilter filter);
|
||||||
|
|
||||||
/** Set maximum number of rows returned, -1 if unlimited. */
|
|
||||||
void setLimit(int limit);
|
|
||||||
|
|
||||||
/** Set whether to show conflicted transactions. */
|
/** Set whether to show conflicted transactions. */
|
||||||
void setShowInactive(bool showInactive);
|
void setShowInactive(bool showInactive);
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override;
|
bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override;
|
||||||
|
|
||||||
|
@ -60,7 +55,6 @@ private:
|
||||||
quint32 typeFilter;
|
quint32 typeFilter;
|
||||||
WatchOnlyFilter watchOnlyFilter;
|
WatchOnlyFilter watchOnlyFilter;
|
||||||
CAmount minAmount;
|
CAmount minAmount;
|
||||||
int limitRows;
|
|
||||||
bool showInactive;
|
bool showInactive;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
#ifdef ARENA_DEBUG
|
#ifdef ARENA_DEBUG
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
#ifndef BITCOIN_SUPPORT_LOCKEDPOOL_H
|
#ifndef BITCOIN_SUPPORT_LOCKEDPOOL_H
|
||||||
#define BITCOIN_SUPPORT_LOCKEDPOOL_H
|
#define BITCOIN_SUPPORT_LOCKEDPOOL_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <cstddef>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <mutex>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -441,4 +441,18 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
|
||||||
fs::remove(streams_test_filename);
|
fs::remove(streams_test_filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(streams_hashed)
|
||||||
|
{
|
||||||
|
CDataStream stream(SER_NETWORK, INIT_PROTO_VERSION);
|
||||||
|
HashedSourceWriter hash_writer{stream};
|
||||||
|
const std::string data{"bitcoin"};
|
||||||
|
hash_writer << data;
|
||||||
|
|
||||||
|
CHashVerifier hash_verifier{&stream};
|
||||||
|
std::string result;
|
||||||
|
hash_verifier >> result;
|
||||||
|
BOOST_CHECK_EQUAL(data, result);
|
||||||
|
BOOST_CHECK_EQUAL(hash_writer.GetHash(), hash_verifier.GetHash());
|
||||||
|
}
|
||||||
|
|
||||||
BOOST_AUTO_TEST_SUITE_END()
|
BOOST_AUTO_TEST_SUITE_END()
|
||||||
|
|
|
@ -709,9 +709,12 @@ static RPCHelpMan migratewallet()
|
||||||
"A new wallet backup will need to be made.\n"
|
"A new wallet backup will need to be made.\n"
|
||||||
"\nThe migration process will create a backup of the wallet before migrating. This backup\n"
|
"\nThe migration process will create a backup of the wallet before migrating. This backup\n"
|
||||||
"file will be named <wallet name>-<timestamp>.legacy.bak and can be found in the directory\n"
|
"file will be named <wallet name>-<timestamp>.legacy.bak and can be found in the directory\n"
|
||||||
"for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet." +
|
"for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet."
|
||||||
HELP_REQUIRING_PASSPHRASE,
|
"\nEncrypted wallets must have the passphrase provided as an argument to this call.",
|
||||||
{},
|
{
|
||||||
|
{"wallet_name", RPCArg::Type::STR, RPCArg::DefaultHint{"the wallet name from the RPC endpoint"}, "The name of the wallet to migrate. If provided both here and in the RPC endpoint, the two must be identical."},
|
||||||
|
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"},
|
||||||
|
},
|
||||||
RPCResult{
|
RPCResult{
|
||||||
RPCResult::Type::OBJ, "", "",
|
RPCResult::Type::OBJ, "", "",
|
||||||
{
|
{
|
||||||
|
@ -727,16 +730,26 @@ static RPCHelpMan migratewallet()
|
||||||
},
|
},
|
||||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||||
{
|
{
|
||||||
std::shared_ptr<CWallet> wallet = GetWalletForJSONRPCRequest(request);
|
std::string wallet_name;
|
||||||
if (!wallet) return NullUniValue;
|
if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) {
|
||||||
|
if (!(request.params[0].isNull() || request.params[0].get_str() == wallet_name)) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "RPC endpoint wallet and wallet_name parameter specify different wallets");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (request.params[0].isNull()) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Either RPC endpoint wallet or wallet_name parameter must be provided");
|
||||||
|
}
|
||||||
|
wallet_name = request.params[0].get_str();
|
||||||
|
}
|
||||||
|
|
||||||
if (wallet->IsCrypted()) {
|
SecureString wallet_pass;
|
||||||
throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: migratewallet on encrypted wallets is currently unsupported.");
|
wallet_pass.reserve(100);
|
||||||
|
if (!request.params[1].isNull()) {
|
||||||
|
wallet_pass = std::string_view{request.params[1].get_str()};
|
||||||
}
|
}
|
||||||
|
|
||||||
WalletContext& context = EnsureWalletContext(request.context);
|
WalletContext& context = EnsureWalletContext(request.context);
|
||||||
|
util::Result<MigrationResult> res = MigrateLegacyToDescriptor(wallet_name, wallet_pass, context);
|
||||||
util::Result<MigrationResult> res = MigrateLegacyToDescriptor(std::move(wallet), context);
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
|
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1074,6 +1074,13 @@ util::Result<CreatedTransactionResult> CreateTransaction(
|
||||||
TRACE1(coin_selection, attempting_aps_create_tx, wallet.GetName().c_str());
|
TRACE1(coin_selection, attempting_aps_create_tx, wallet.GetName().c_str());
|
||||||
CCoinControl tmp_cc = coin_control;
|
CCoinControl tmp_cc = coin_control;
|
||||||
tmp_cc.m_avoid_partial_spends = true;
|
tmp_cc.m_avoid_partial_spends = true;
|
||||||
|
|
||||||
|
// Re-use the change destination from the first creation attempt to avoid skipping BIP44 indexes
|
||||||
|
const int ungrouped_change_pos = txr_ungrouped.change_pos;
|
||||||
|
if (ungrouped_change_pos != -1) {
|
||||||
|
ExtractDestination(txr_ungrouped.tx->vout[ungrouped_change_pos].scriptPubKey, tmp_cc.destChange);
|
||||||
|
}
|
||||||
|
|
||||||
auto txr_grouped = CreateTransactionInternal(wallet, vecSend, change_pos, tmp_cc, sign);
|
auto txr_grouped = CreateTransactionInternal(wallet, vecSend, change_pos, tmp_cc, sign);
|
||||||
// if fee of this alternative one is within the range of the max fee, we use this one
|
// if fee of this alternative one is within the range of the max fee, we use this one
|
||||||
const bool use_aps{txr_grouped.has_value() ? (txr_grouped->fee <= txr_ungrouped.fee + wallet.m_max_aps_fee) : false};
|
const bool use_aps{txr_grouped.has_value() ? (txr_grouped->fee <= txr_ungrouped.fee + wallet.m_max_aps_fee) : false};
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
#include <script/descriptor.h>
|
#include <script/descriptor.h>
|
||||||
#include <script/script.h>
|
#include <script/script.h>
|
||||||
#include <script/signingprovider.h>
|
#include <script/signingprovider.h>
|
||||||
|
#include <support/cleanse.h>
|
||||||
#include <txmempool.h>
|
#include <txmempool.h>
|
||||||
#include <util/bip32.h>
|
#include <util/bip32.h>
|
||||||
#include <util/check.h>
|
#include <util/check.h>
|
||||||
|
@ -589,8 +590,7 @@ bool CWallet::HasWalletSpend(const CTransactionRef& tx) const
|
||||||
AssertLockHeld(cs_wallet);
|
AssertLockHeld(cs_wallet);
|
||||||
const uint256& txid = tx->GetHash();
|
const uint256& txid = tx->GetHash();
|
||||||
for (unsigned int i = 0; i < tx->vout.size(); ++i) {
|
for (unsigned int i = 0; i < tx->vout.size(); ++i) {
|
||||||
auto iter = mapTxSpends.find(COutPoint(txid, i));
|
if (IsSpent(COutPoint(txid, i))) {
|
||||||
if (iter != mapTxSpends.end()) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3102,6 +3102,24 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
|
||||||
|
|
||||||
if (tip_height && *tip_height != rescan_height)
|
if (tip_height && *tip_height != rescan_height)
|
||||||
{
|
{
|
||||||
|
// No need to read and scan block if block was created before
|
||||||
|
// our wallet birthday (as adjusted for block time variability)
|
||||||
|
std::optional<int64_t> time_first_key;
|
||||||
|
for (auto spk_man : walletInstance->GetAllScriptPubKeyMans()) {
|
||||||
|
int64_t time = spk_man->GetTimeFirstKey();
|
||||||
|
if (!time_first_key || time < *time_first_key) time_first_key = time;
|
||||||
|
}
|
||||||
|
if (time_first_key) {
|
||||||
|
FoundBlock found = FoundBlock().height(rescan_height);
|
||||||
|
chain.findFirstBlockWithTimeAndHeight(*time_first_key - TIMESTAMP_WINDOW, rescan_height, found);
|
||||||
|
if (!found.found) {
|
||||||
|
// We were unable to find a block that had a time more recent than our earliest timestamp
|
||||||
|
// or a height higher than the wallet was synced to, indicating that the wallet is newer than the
|
||||||
|
// current chain tip. Skip rescanning in this case.
|
||||||
|
rescan_height = *tip_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Technically we could execute the code below in any case, but performing the
|
// Technically we could execute the code below in any case, but performing the
|
||||||
// `while` loop below can make startup very slow, so only check blocks on disk
|
// `while` loop below can make startup very slow, so only check blocks on disk
|
||||||
// if necessary.
|
// if necessary.
|
||||||
|
@ -3134,17 +3152,6 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
|
||||||
chain.initMessage(_("Rescanning…").translated);
|
chain.initMessage(_("Rescanning…").translated);
|
||||||
walletInstance->WalletLogPrintf("Rescanning last %i blocks (from block %i)...\n", *tip_height - rescan_height, rescan_height);
|
walletInstance->WalletLogPrintf("Rescanning last %i blocks (from block %i)...\n", *tip_height - rescan_height, rescan_height);
|
||||||
|
|
||||||
// No need to read and scan block if block was created before
|
|
||||||
// our wallet birthday (as adjusted for block time variability)
|
|
||||||
std::optional<int64_t> time_first_key;
|
|
||||||
for (auto spk_man : walletInstance->GetAllScriptPubKeyMans()) {
|
|
||||||
int64_t time = spk_man->GetTimeFirstKey();
|
|
||||||
if (!time_first_key || time < *time_first_key) time_first_key = time;
|
|
||||||
}
|
|
||||||
if (time_first_key) {
|
|
||||||
chain.findFirstBlockWithTimeAndHeight(*time_first_key - TIMESTAMP_WINDOW, rescan_height, FoundBlock().height(rescan_height));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
WalletRescanReserver reserver(*walletInstance);
|
WalletRescanReserver reserver(*walletInstance);
|
||||||
if (!reserver.reserve() || (ScanResult::SUCCESS != walletInstance->ScanForWalletTransactions(chain.getBlockHash(rescan_height), rescan_height, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/true).status)) {
|
if (!reserver.reserve() || (ScanResult::SUCCESS != walletInstance->ScanForWalletTransactions(chain.getBlockHash(rescan_height), rescan_height, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/true).status)) {
|
||||||
|
@ -3287,7 +3294,10 @@ bool CWallet::Lock()
|
||||||
|
|
||||||
{
|
{
|
||||||
LOCK(cs_wallet);
|
LOCK(cs_wallet);
|
||||||
vMasterKey.clear();
|
if (!vMasterKey.empty()) {
|
||||||
|
memory_cleanse(vMasterKey.data(), vMasterKey.size() * sizeof(decltype(vMasterKey)::value_type));
|
||||||
|
vMasterKey.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyStatusChanged(this);
|
NotifyStatusChanged(this);
|
||||||
|
@ -3757,7 +3767,7 @@ std::optional<MigrationData> CWallet::GetDescriptorsForLegacy(bilingual_str& err
|
||||||
|
|
||||||
std::optional<MigrationData> res = legacy_spkm->MigrateToDescriptor();
|
std::optional<MigrationData> res = legacy_spkm->MigrateToDescriptor();
|
||||||
if (res == std::nullopt) {
|
if (res == std::nullopt) {
|
||||||
error = _("Error: Unable to produce descriptors for this legacy wallet. Make sure the wallet is unlocked first");
|
error = _("Error: Unable to produce descriptors for this legacy wallet. Make sure to provide the wallet's passphrase if it is encrypted.");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
|
@ -3918,6 +3928,23 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist added address book entries (labels, purpose) for watchonly and solvable wallets
|
||||||
|
auto persist_address_book = [](const CWallet& wallet) {
|
||||||
|
LOCK(wallet.cs_wallet);
|
||||||
|
WalletBatch batch{wallet.GetDatabase()};
|
||||||
|
for (const auto& [destination, addr_book_data] : wallet.m_address_book) {
|
||||||
|
auto address{EncodeDestination(destination)};
|
||||||
|
auto purpose{addr_book_data.purpose};
|
||||||
|
auto label{addr_book_data.GetLabel()};
|
||||||
|
// don't bother writing default values (unknown purpose, empty label)
|
||||||
|
if (purpose != "unknown") batch.WritePurpose(address, purpose);
|
||||||
|
if (!label.empty()) batch.WriteName(address, label);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (data.watchonly_wallet) persist_address_book(*data.watchonly_wallet);
|
||||||
|
if (data.solvable_wallet) persist_address_book(*data.solvable_wallet);
|
||||||
|
|
||||||
// Remove the things to delete
|
// Remove the things to delete
|
||||||
if (dests_to_delete.size() > 0) {
|
if (dests_to_delete.size() > 0) {
|
||||||
for (const auto& dest : dests_to_delete) {
|
for (const auto& dest : dests_to_delete) {
|
||||||
|
@ -4030,27 +4057,19 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>&& wallet, WalletContext& context)
|
util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context)
|
||||||
{
|
{
|
||||||
MigrationResult res;
|
MigrationResult res;
|
||||||
bilingual_str error;
|
bilingual_str error;
|
||||||
std::vector<bilingual_str> warnings;
|
std::vector<bilingual_str> warnings;
|
||||||
|
|
||||||
// Make a backup of the DB
|
// If the wallet is still loaded, unload it so that nothing else tries to use it while we're changing it
|
||||||
std::string wallet_name = wallet->GetName();
|
if (auto wallet = GetWallet(context, wallet_name)) {
|
||||||
fs::path this_wallet_dir = fs::absolute(fs::PathFromString(wallet->GetDatabase().Filename())).parent_path();
|
if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
|
||||||
fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime()));
|
return util::Error{_("Unable to unload the wallet before migrating")};
|
||||||
fs::path backup_path = this_wallet_dir / backup_filename;
|
}
|
||||||
if (!wallet->BackupWallet(fs::PathToString(backup_path))) {
|
UnloadWallet(std::move(wallet));
|
||||||
return util::Error{_("Error: Unable to make a backup of your wallet")};
|
|
||||||
}
|
}
|
||||||
res.backup_path = backup_path;
|
|
||||||
|
|
||||||
// Unload the wallet so that nothing else tries to use it while we're changing it
|
|
||||||
if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
|
|
||||||
return util::Error{_("Unable to unload the wallet before migrating")};
|
|
||||||
}
|
|
||||||
UnloadWallet(std::move(wallet));
|
|
||||||
|
|
||||||
// Load the wallet but only in the context of this function.
|
// Load the wallet but only in the context of this function.
|
||||||
// No signals should be connected nor should anything else be aware of this wallet
|
// No signals should be connected nor should anything else be aware of this wallet
|
||||||
|
@ -4064,15 +4083,43 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
|
||||||
return util::Error{Untranslated("Wallet file verification failed.") + Untranslated(" ") + error};
|
return util::Error{Untranslated("Wallet file verification failed.") + Untranslated(" ") + error};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make the local wallet
|
||||||
std::shared_ptr<CWallet> local_wallet = CWallet::Create(empty_context, wallet_name, std::move(database), options.create_flags, error, warnings);
|
std::shared_ptr<CWallet> local_wallet = CWallet::Create(empty_context, wallet_name, std::move(database), options.create_flags, error, warnings);
|
||||||
if (!local_wallet) {
|
if (!local_wallet) {
|
||||||
return util::Error{Untranslated("Wallet loading failed.") + Untranslated(" ") + error};
|
return util::Error{Untranslated("Wallet loading failed.") + Untranslated(" ") + error};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before anything else, check if there is something to migrate.
|
||||||
|
if (!local_wallet->GetLegacyScriptPubKeyMan()) {
|
||||||
|
return util::Error{_("Error: This wallet is already a descriptor wallet")};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a backup of the DB
|
||||||
|
fs::path this_wallet_dir = fs::absolute(fs::PathFromString(local_wallet->GetDatabase().Filename())).parent_path();
|
||||||
|
fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime()));
|
||||||
|
fs::path backup_path = this_wallet_dir / backup_filename;
|
||||||
|
if (!local_wallet->BackupWallet(fs::PathToString(backup_path))) {
|
||||||
|
return util::Error{_("Error: Unable to make a backup of your wallet")};
|
||||||
|
}
|
||||||
|
res.backup_path = backup_path;
|
||||||
|
|
||||||
bool success = false;
|
bool success = false;
|
||||||
{
|
{
|
||||||
LOCK(local_wallet->cs_wallet);
|
LOCK(local_wallet->cs_wallet);
|
||||||
|
|
||||||
|
// Unlock the wallet if needed
|
||||||
|
if (local_wallet->IsLocked() && !local_wallet->Unlock(passphrase)) {
|
||||||
|
if (passphrase.find('\0') == std::string::npos) {
|
||||||
|
return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect.")};
|
||||||
|
} else {
|
||||||
|
return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase entered was incorrect. "
|
||||||
|
"The passphrase contains a null character (ie - a zero byte). "
|
||||||
|
"If this passphrase was set with a version of this software prior to 25.0, "
|
||||||
|
"please try again with only the characters up to — but not including — "
|
||||||
|
"the first null character.")};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// First change to using SQLite
|
// First change to using SQLite
|
||||||
if (!local_wallet->MigrateToSQLite(error)) return util::Error{error};
|
if (!local_wallet->MigrateToSQLite(error)) return util::Error{error};
|
||||||
|
|
||||||
|
|
|
@ -1006,7 +1006,7 @@ struct MigrationResult {
|
||||||
};
|
};
|
||||||
|
|
||||||
//! Do all steps to migrate a legacy wallet to a descriptor wallet
|
//! Do all steps to migrate a legacy wallet to a descriptor wallet
|
||||||
util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>&& wallet, WalletContext& context);
|
util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context);
|
||||||
} // namespace wallet
|
} // namespace wallet
|
||||||
|
|
||||||
#endif // BITCOIN_WALLET_WALLET_H
|
#endif // BITCOIN_WALLET_WALLET_H
|
||||||
|
|
|
@ -199,6 +199,8 @@ BASE_SCRIPTS = [
|
||||||
'rpc_blockchain.py',
|
'rpc_blockchain.py',
|
||||||
'rpc_deprecated.py',
|
'rpc_deprecated.py',
|
||||||
'wallet_disable.py',
|
'wallet_disable.py',
|
||||||
|
'wallet_change_address.py --legacy-wallet',
|
||||||
|
'wallet_change_address.py --descriptors',
|
||||||
'p2p_addr_relay.py',
|
'p2p_addr_relay.py',
|
||||||
'p2p_getaddr_caching.py',
|
'p2p_getaddr_caching.py',
|
||||||
'p2p_getdata.py',
|
'p2p_getdata.py',
|
||||||
|
|
|
@ -89,6 +89,7 @@ class BumpFeeTest(BitcoinTestFramework):
|
||||||
test_nonrbf_bumpfee_fails(self, peer_node, dest_address)
|
test_nonrbf_bumpfee_fails(self, peer_node, dest_address)
|
||||||
test_notmine_bumpfee(self, rbf_node, peer_node, dest_address)
|
test_notmine_bumpfee(self, rbf_node, peer_node, dest_address)
|
||||||
test_bumpfee_with_descendant_fails(self, rbf_node, rbf_node_address, dest_address)
|
test_bumpfee_with_descendant_fails(self, rbf_node, rbf_node_address, dest_address)
|
||||||
|
test_bumpfee_with_abandoned_descendant_succeeds(self, rbf_node, rbf_node_address, dest_address)
|
||||||
test_dust_to_fee(self, rbf_node, dest_address)
|
test_dust_to_fee(self, rbf_node, dest_address)
|
||||||
test_watchonly_psbt(self, peer_node, rbf_node, dest_address)
|
test_watchonly_psbt(self, peer_node, rbf_node, dest_address)
|
||||||
test_rebumping(self, rbf_node, dest_address)
|
test_rebumping(self, rbf_node, dest_address)
|
||||||
|
@ -294,6 +295,35 @@ def test_bumpfee_with_descendant_fails(self, rbf_node, rbf_node_address, dest_ad
|
||||||
self.clear_mempool()
|
self.clear_mempool()
|
||||||
|
|
||||||
|
|
||||||
|
def test_bumpfee_with_abandoned_descendant_succeeds(self, rbf_node, rbf_node_address, dest_address):
|
||||||
|
self.log.info('Test that fee can be bumped when it has abandoned descendant')
|
||||||
|
# parent is send-to-self, so we don't have to check which output is change when creating the child tx
|
||||||
|
parent_id = spend_one_input(rbf_node, rbf_node_address)
|
||||||
|
# Submit child transaction with low fee
|
||||||
|
child_id = rbf_node.send(outputs={dest_address: 0.00020000},
|
||||||
|
options={"inputs": [{"txid": parent_id, "vout": 0}], "fee_rate": 2})["txid"]
|
||||||
|
assert child_id in rbf_node.getrawmempool()
|
||||||
|
|
||||||
|
# Restart the node with higher min relay fee so the descendant tx is no longer in mempool so that we can abandon it
|
||||||
|
self.restart_node(1, ['-minrelaytxfee=0.00005'] + self.extra_args[1])
|
||||||
|
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
||||||
|
self.connect_nodes(1, 0)
|
||||||
|
assert parent_id in rbf_node.getrawmempool()
|
||||||
|
assert child_id not in rbf_node.getrawmempool()
|
||||||
|
# Should still raise an error even if not in mempool
|
||||||
|
assert_raises_rpc_error(-8, "Transaction has descendants in the wallet", rbf_node.bumpfee, parent_id)
|
||||||
|
# Now abandon the child transaction and bump the original
|
||||||
|
rbf_node.abandontransaction(child_id)
|
||||||
|
bumped_result = rbf_node.bumpfee(parent_id, {"fee_rate": HIGH})
|
||||||
|
assert bumped_result['txid'] in rbf_node.getrawmempool()
|
||||||
|
assert parent_id not in rbf_node.getrawmempool()
|
||||||
|
# Cleanup
|
||||||
|
self.restart_node(1, self.extra_args[1])
|
||||||
|
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
||||||
|
self.connect_nodes(1, 0)
|
||||||
|
self.clear_mempool()
|
||||||
|
|
||||||
|
|
||||||
def test_small_output_with_feerate_succeeds(self, rbf_node, dest_address):
|
def test_small_output_with_feerate_succeeds(self, rbf_node, dest_address):
|
||||||
self.log.info('Testing small output with feerate bump succeeds')
|
self.log.info('Testing small output with feerate bump succeeds')
|
||||||
|
|
||||||
|
|
105
test/functional/wallet_change_address.py
Executable file
105
test/functional/wallet_change_address.py
Executable file
|
@ -0,0 +1,105 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2023 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
"""Test wallet change address selection"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from test_framework.blocktools import COINBASE_MATURITY
|
||||||
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
|
from test_framework.util import (
|
||||||
|
assert_equal,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WalletChangeAddressTest(BitcoinTestFramework):
|
||||||
|
def set_test_params(self):
|
||||||
|
self.setup_clean_chain = True
|
||||||
|
self.num_nodes = 3
|
||||||
|
# discardfee is used to make change outputs less likely in the change_pos test
|
||||||
|
self.extra_args = [
|
||||||
|
[],
|
||||||
|
["-discardfee=1"],
|
||||||
|
["-avoidpartialspends", "-discardfee=1"]
|
||||||
|
]
|
||||||
|
|
||||||
|
def skip_test_if_missing_module(self):
|
||||||
|
self.skip_if_no_wallet()
|
||||||
|
|
||||||
|
def assert_change_index(self, node, tx, index):
|
||||||
|
change_index = None
|
||||||
|
for vout in tx["vout"]:
|
||||||
|
info = node.getaddressinfo(vout["scriptPubKey"]["address"])
|
||||||
|
if (info["ismine"] and info["ischange"]):
|
||||||
|
change_index = int(re.findall(r'\d+', info["hdkeypath"])[-1])
|
||||||
|
break
|
||||||
|
assert_equal(change_index, index)
|
||||||
|
|
||||||
|
def assert_change_pos(self, wallet, tx, pos):
|
||||||
|
change_pos = None
|
||||||
|
for index, output in enumerate(tx["vout"]):
|
||||||
|
info = wallet.getaddressinfo(output["scriptPubKey"]["address"])
|
||||||
|
if (info["ismine"] and info["ischange"]):
|
||||||
|
change_pos = index
|
||||||
|
break
|
||||||
|
assert_equal(change_pos, pos)
|
||||||
|
|
||||||
|
def run_test(self):
|
||||||
|
self.log.info("Setting up")
|
||||||
|
# Mine some coins
|
||||||
|
self.generate(self.nodes[0], COINBASE_MATURITY + 1)
|
||||||
|
|
||||||
|
# Get some addresses from the two nodes
|
||||||
|
addr1 = [self.nodes[1].getnewaddress() for _ in range(3)]
|
||||||
|
addr2 = [self.nodes[2].getnewaddress() for _ in range(3)]
|
||||||
|
addrs = addr1 + addr2
|
||||||
|
|
||||||
|
# Send 1 + 0.5 coin to each address
|
||||||
|
[self.nodes[0].sendtoaddress(addr, 1.0) for addr in addrs]
|
||||||
|
[self.nodes[0].sendtoaddress(addr, 0.5) for addr in addrs]
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
|
||||||
|
for i in range(20):
|
||||||
|
for n in [1, 2]:
|
||||||
|
self.log.debug(f"Send transaction from node {n}: expected change index {i}")
|
||||||
|
txid = self.nodes[n].sendtoaddress(self.nodes[0].getnewaddress(), 0.2)
|
||||||
|
tx = self.nodes[n].getrawtransaction(txid, True)
|
||||||
|
# find the change output and ensure that expected change index was used
|
||||||
|
self.assert_change_index(self.nodes[n], tx, i)
|
||||||
|
|
||||||
|
# Start next test with fresh wallets and new coins
|
||||||
|
self.nodes[1].createwallet("w1")
|
||||||
|
self.nodes[2].createwallet("w2")
|
||||||
|
w1 = self.nodes[1].get_wallet_rpc("w1")
|
||||||
|
w2 = self.nodes[2].get_wallet_rpc("w2")
|
||||||
|
addr1 = w1.getnewaddress()
|
||||||
|
addr2 = w2.getnewaddress()
|
||||||
|
self.nodes[0].sendtoaddress(addr1, 3.0)
|
||||||
|
self.nodes[0].sendtoaddress(addr1, 0.1)
|
||||||
|
self.nodes[0].sendtoaddress(addr2, 3.0)
|
||||||
|
self.nodes[0].sendtoaddress(addr2, 0.1)
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
|
||||||
|
sendTo1 = self.nodes[0].getnewaddress()
|
||||||
|
sendTo2 = self.nodes[0].getnewaddress()
|
||||||
|
sendTo3 = self.nodes[0].getnewaddress()
|
||||||
|
|
||||||
|
# The avoid partial spends wallet will always create a change output
|
||||||
|
node = self.nodes[2]
|
||||||
|
res = w2.send(outputs=[{sendTo1: 1.0}, {sendTo2: 1.0}, {sendTo3: 0.9999}], options={"change_position": 0})
|
||||||
|
tx = node.getrawtransaction(res["txid"], True)
|
||||||
|
self.assert_change_pos(w2, tx, 0)
|
||||||
|
|
||||||
|
# The default wallet will internally create a tx without change first,
|
||||||
|
# then create a second candidate using APS that requires a change output.
|
||||||
|
# Ensure that the user-configured change position is kept
|
||||||
|
node = self.nodes[1]
|
||||||
|
res = w1.send(outputs=[{sendTo1: 1.0}, {sendTo2: 1.0}, {sendTo3: 0.9999}], options={"change_position": 0})
|
||||||
|
tx = node.getrawtransaction(res["txid"], True)
|
||||||
|
# If the wallet ignores the user's change_position there is still a 25%
|
||||||
|
# that the random change position passes the test
|
||||||
|
self.assert_change_pos(w1, tx, 0)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
WalletChangeAddressTest().main()
|
|
@ -447,14 +447,14 @@ class ImportDescriptorsTest(BitcoinTestFramework):
|
||||||
wallet=wmulti_priv)
|
wallet=wmulti_priv)
|
||||||
|
|
||||||
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1001) # Range end (1000) is inclusive, so 1001 addresses generated
|
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1001) # Range end (1000) is inclusive, so 1001 addresses generated
|
||||||
addr = wmulti_priv.getnewaddress('', 'bech32')
|
addr = wmulti_priv.getnewaddress('', 'bech32') # uses receive 0
|
||||||
assert_equal(addr, 'bcrt1qdt0qy5p7dzhxzmegnn4ulzhard33s2809arjqgjndx87rv5vd0fq2czhy8') # Derived at m/84'/0'/0'/0
|
assert_equal(addr, 'bcrt1qdt0qy5p7dzhxzmegnn4ulzhard33s2809arjqgjndx87rv5vd0fq2czhy8') # Derived at m/84'/0'/0'/0
|
||||||
change_addr = wmulti_priv.getrawchangeaddress('bech32')
|
change_addr = wmulti_priv.getrawchangeaddress('bech32') # uses change 0
|
||||||
assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e')
|
assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e') # Derived at m/84'/1'/0'/0
|
||||||
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000)
|
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000)
|
||||||
txid = w0.sendtoaddress(addr, 10)
|
txid = w0.sendtoaddress(addr, 10)
|
||||||
self.generate(self.nodes[0], 6)
|
self.generate(self.nodes[0], 6)
|
||||||
send_txid = wmulti_priv.sendtoaddress(w0.getnewaddress(), 8)
|
send_txid = wmulti_priv.sendtoaddress(w0.getnewaddress(), 8) # uses change 1
|
||||||
decoded = wmulti_priv.gettransaction(txid=send_txid, verbose=True)['decoded']
|
decoded = wmulti_priv.gettransaction(txid=send_txid, verbose=True)['decoded']
|
||||||
assert_equal(len(decoded['vin'][0]['txinwitness']), 4)
|
assert_equal(len(decoded['vin'][0]['txinwitness']), 4)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
@ -480,12 +480,12 @@ class ImportDescriptorsTest(BitcoinTestFramework):
|
||||||
wallet=wmulti_pub)
|
wallet=wmulti_pub)
|
||||||
|
|
||||||
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 1000) # The first one was already consumed by previous import and is detected as used
|
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 1000) # The first one was already consumed by previous import and is detected as used
|
||||||
addr = wmulti_pub.getnewaddress('', 'bech32')
|
addr = wmulti_pub.getnewaddress('', 'bech32') # uses receive 1
|
||||||
assert_equal(addr, 'bcrt1qp8s25ckjl7gr6x2q3dx3tn2pytwp05upkjztk6ey857tt50r5aeqn6mvr9') # Derived at m/84'/0'/0'/1
|
assert_equal(addr, 'bcrt1qp8s25ckjl7gr6x2q3dx3tn2pytwp05upkjztk6ey857tt50r5aeqn6mvr9') # Derived at m/84'/0'/0'/1
|
||||||
change_addr = wmulti_pub.getrawchangeaddress('bech32')
|
change_addr = wmulti_pub.getrawchangeaddress('bech32') # uses change 2
|
||||||
assert_equal(change_addr, 'bcrt1qzxl0qz2t88kljdnkzg4n4gapr6kte26390gttrg79x66nt4p04fssj53nl')
|
assert_equal(change_addr, 'bcrt1qp6j3jw8yetefte7kw6v5pc89rkgakzy98p6gf7ayslaveaxqyjusnw580c') # Derived at m/84'/1'/0'/2
|
||||||
assert(send_txid in self.nodes[0].getrawmempool(True))
|
assert send_txid in self.nodes[0].getrawmempool(True)
|
||||||
assert(send_txid in (x['txid'] for x in wmulti_pub.listunspent(0)))
|
assert send_txid in (x['txid'] for x in wmulti_pub.listunspent(0))
|
||||||
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999)
|
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999)
|
||||||
|
|
||||||
# generate some utxos for next tests
|
# generate some utxos for next tests
|
||||||
|
|
|
@ -257,7 +257,7 @@ class WalletMigrationTest(BitcoinTestFramework):
|
||||||
imports0 = self.nodes[0].get_wallet_rpc("imports0")
|
imports0 = self.nodes[0].get_wallet_rpc("imports0")
|
||||||
assert_equal(imports0.getwalletinfo()["descriptors"], False)
|
assert_equal(imports0.getwalletinfo()["descriptors"], False)
|
||||||
|
|
||||||
# Exteranl address label
|
# External address label
|
||||||
imports0.setlabel(default.getnewaddress(), "external")
|
imports0.setlabel(default.getnewaddress(), "external")
|
||||||
|
|
||||||
# Normal non-watchonly tx
|
# Normal non-watchonly tx
|
||||||
|
@ -310,6 +310,13 @@ class WalletMigrationTest(BitcoinTestFramework):
|
||||||
assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", watchonly.gettransaction, received_txid)
|
assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", watchonly.gettransaction, received_txid)
|
||||||
assert_equal(len(watchonly.listtransactions(include_watchonly=True)), 3)
|
assert_equal(len(watchonly.listtransactions(include_watchonly=True)), 3)
|
||||||
|
|
||||||
|
# Check that labels were migrated and persisted to watchonly wallet
|
||||||
|
self.nodes[0].unloadwallet("imports0_watchonly")
|
||||||
|
self.nodes[0].loadwallet("imports0_watchonly")
|
||||||
|
labels = watchonly.listlabels()
|
||||||
|
assert "external" in labels
|
||||||
|
assert "imported" in labels
|
||||||
|
|
||||||
def test_no_privkeys(self):
|
def test_no_privkeys(self):
|
||||||
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
|
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
|
||||||
|
|
||||||
|
@ -396,11 +403,75 @@ class WalletMigrationTest(BitcoinTestFramework):
|
||||||
def test_encrypted(self):
|
def test_encrypted(self):
|
||||||
self.log.info("Test migration of an encrypted wallet")
|
self.log.info("Test migration of an encrypted wallet")
|
||||||
wallet = self.create_legacy_wallet("encrypted")
|
wallet = self.create_legacy_wallet("encrypted")
|
||||||
|
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
|
||||||
|
|
||||||
wallet.encryptwallet("pass")
|
wallet.encryptwallet("pass")
|
||||||
|
addr = wallet.getnewaddress()
|
||||||
|
txid = default.sendtoaddress(addr, 1)
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
bals = wallet.getbalances()
|
||||||
|
|
||||||
assert_raises_rpc_error(-15, "Error: migratewallet on encrypted wallets is currently unsupported.", wallet.migratewallet)
|
assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", wallet.migratewallet)
|
||||||
# TODO: Fix migratewallet so that we can actually migrate encrypted wallets
|
assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", wallet.migratewallet, None, "badpass")
|
||||||
|
assert_raises_rpc_error(-4, "The passphrase contains a null character", wallet.migratewallet, None, "pass\0with\0null")
|
||||||
|
|
||||||
|
wallet.migratewallet(passphrase="pass")
|
||||||
|
|
||||||
|
info = wallet.getwalletinfo()
|
||||||
|
assert_equal(info["descriptors"], True)
|
||||||
|
assert_equal(info["format"], "sqlite")
|
||||||
|
assert_equal(info["unlocked_until"], 0)
|
||||||
|
wallet.gettransaction(txid)
|
||||||
|
|
||||||
|
assert_equal(bals, wallet.getbalances())
|
||||||
|
|
||||||
|
def test_unloaded(self):
|
||||||
|
self.log.info("Test migration of a wallet that isn't loaded")
|
||||||
|
wallet = self.create_legacy_wallet("notloaded")
|
||||||
|
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
|
||||||
|
|
||||||
|
addr = wallet.getnewaddress()
|
||||||
|
txid = default.sendtoaddress(addr, 1)
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
bals = wallet.getbalances()
|
||||||
|
|
||||||
|
wallet.unloadwallet()
|
||||||
|
|
||||||
|
assert_raises_rpc_error(-8, "RPC endpoint wallet and wallet_name parameter specify different wallets", wallet.migratewallet, "someotherwallet")
|
||||||
|
assert_raises_rpc_error(-8, "Either RPC endpoint wallet or wallet_name parameter must be provided", self.nodes[0].migratewallet)
|
||||||
|
self.nodes[0].migratewallet("notloaded")
|
||||||
|
|
||||||
|
info = wallet.getwalletinfo()
|
||||||
|
assert_equal(info["descriptors"], True)
|
||||||
|
assert_equal(info["format"], "sqlite")
|
||||||
|
wallet.gettransaction(txid)
|
||||||
|
|
||||||
|
assert_equal(bals, wallet.getbalances())
|
||||||
|
|
||||||
|
def test_unloaded_by_path(self):
|
||||||
|
self.log.info("Test migration of a wallet that isn't loaded, specified by path")
|
||||||
|
wallet = self.create_legacy_wallet("notloaded2")
|
||||||
|
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
|
||||||
|
|
||||||
|
addr = wallet.getnewaddress()
|
||||||
|
txid = default.sendtoaddress(addr, 1)
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
bals = wallet.getbalances()
|
||||||
|
|
||||||
|
wallet.unloadwallet()
|
||||||
|
|
||||||
|
wallet_file_path = os.path.join(self.nodes[0].datadir, "regtest", "wallets", "notloaded2")
|
||||||
|
self.nodes[0].migratewallet(wallet_file_path)
|
||||||
|
|
||||||
|
# Because we gave the name by full path, the loaded wallet's name is that path too.
|
||||||
|
wallet = self.nodes[0].get_wallet_rpc(wallet_file_path)
|
||||||
|
|
||||||
|
info = wallet.getwalletinfo()
|
||||||
|
assert_equal(info["descriptors"], True)
|
||||||
|
assert_equal(info["format"], "sqlite")
|
||||||
|
wallet.gettransaction(txid)
|
||||||
|
|
||||||
|
assert_equal(bals, wallet.getbalances())
|
||||||
|
|
||||||
def run_test(self):
|
def run_test(self):
|
||||||
self.generate(self.nodes[0], 101)
|
self.generate(self.nodes[0], 101)
|
||||||
|
@ -412,6 +483,8 @@ class WalletMigrationTest(BitcoinTestFramework):
|
||||||
self.test_no_privkeys()
|
self.test_no_privkeys()
|
||||||
self.test_pk_coinbases()
|
self.test_pk_coinbases()
|
||||||
self.test_encrypted()
|
self.test_encrypted()
|
||||||
|
self.test_unloaded()
|
||||||
|
self.test_unloaded_by_path()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
WalletMigrationTest().main()
|
WalletMigrationTest().main()
|
||||||
|
|
Loading…
Add table
Reference in a new issue